@sema-agent/core
Version:
Stateless, task-oriented AI agent core
162 lines (161 loc) • 11.5 kB
TypeScript
/**
* A delegated AGENT's definition and the skills it may mount: `AgentDefinition` (the agent a subagent
* tool spawns) and `SkillSpec` (the wired form the runner mounts). Layer 0 vocabulary; `types.ts`
* re-exports the two, so no consumer's import changes.
*/
import type { ThinkingLevel } from "../internal/harness.js";
import type { ModelRef } from "./model-seat.js";
import type { TaskSpec } from "./task-spec.js";
/**
* A declarative, reusable named sub-agent (design/38 1B). Hand a set to {@link createSubagentTool} and the
* model picks one (by `whenToUse`) to delegate to; the chosen definition's config builds the child task.
*
* **Deployment-side config, NOT user input** — `systemPrompt`/`allowTools` shape what the sub-agent can do
* and see, so they must be set by the deployment, never taken from an end-user message.
*/
export interface AgentDefinition {
/** Stable identifier + the value the delegation tool's `agent` parameter selects. Must be unique. */
name: string;
/** Guidance shown to the model for WHEN to pick this agent (listed in the delegation tool's description). */
whenToUse?: string;
/**
* F4 (CC 2.1.198 parity, `whenToUseLean` — 锚 pretty.js:409174 `rbm` + selection `tIl` :453029
* `(lean && e.whenToUseLean) || e.whenToUse`): a SHORT variant of {@link whenToUse} preferred on
* lean-prompt surfaces. sema posture: the engine runs the CC lean-prompt arm wholesale (web.ts
* lean-card precedent), so roster listings/descriptions prefer this text whenever present;
* {@link whenToUse} remains the fallback. Optional — a single-text agent behaves exactly as before.
*/
whenToUseLean?: string;
/** Tool allowlist over the delegation pool (`["*"]` = all). Replaces (does not merge with) the tool's
* own `allowTools`/`denyTools` when this agent is selected. F1 (CC parity): unlike the TOOL-level
* allow/deny (which never filters the nested delegation tool — that stays `maxDepth`-governed), a
* PER-AGENT allow/deny ALSO governs the nested delegation tool: an agent whose allow/deny excludes
* the delegation tool's name cannot re-delegate (CC Explore/Plan `disallowedTools` include Agent,
* pretty.js:409189/486948). */
allowTools?: string[];
/** Tool denylist (a deny always wins). See {@link allowTools} for the per-agent delegation-tool note. */
denyTools?: string[];
/**
* F7 (CC 2.1.198 parity, per-agent `skills` — 锚 pretty.js:422126 `e.skills ?? []`, GA/ungated in 198:
* CC preloads each named skill's prompt content into the child's opening messages, warning on misses).
* sema shape: full {@link SkillSpec} objects (sema has no ambient skill registry to resolve bare names
* against) threaded into the chosen child's `TaskSpec.skills` — disclosed in its `<skills>` block +
* invocable via the `skill` tool. Deployment-trusted, same trust tier as `systemPrompt`.
*/
skills?: SkillSpec[];
/**
* F7 (CC 2.1.198 parity, per-agent `background` — 锚 pretty.js:453611 `H.background===!0` OR-ed into
* the background predicate, GA/ungated): `true` = this agent ALWAYS launches on the background lane
* (an omitted `run_in_background` no longer means sync, and the per-call parameter cannot force sync —
* CC-same OR semantics). Requires the delegation tool's `background` surface; without one the child
* runs synchronously (CC-same quiet fallback — CC's env kill-switch arm, :453611 `&& !sJt`).
*/
background?: boolean;
/**
* F7 (CC 2.1.198 parity, per-agent `isolation` — 锚 pretty.js:453599 `z = a ?? H.isolation`, GA):
* default isolation for this agent; an explicit per-call `isolation` argument wins. sema supports
* `"worktree"` (CC's `"remote"` arm is a claude.ai-session concern, not ported).
*/
isolation?: "worktree";
/** Model for this agent. **Omit to INHERIT the caller's current model** (design/38 1B finding 7) — the
* recommended default; a concrete `ModelRef` pins it instead. */
model?: ModelRef;
/** Thinking level for this agent. **Omit to INHERIT the caller's current thinking level** (the model
* companion above — a mid-run degrade/boost on the parent carries into new children); a concrete
* level pins it instead. */
thinking?: ThinkingLevel;
/** System prompt / persona for this agent. Falls back to the delegation tool's `systemPrompt`. */
systemPrompt?: string;
/** Per-agent turn cap. Falls back to the delegation tool's `limits.maxTurns`. Must be POSITIVE: a
* non-positive/non-finite value is ignored (CLS-A-3) rather than folded in, because downstream an
* explicit `0` DISABLES the turn cap — the opposite of the tightening this field expresses. */
maxTurns?: number;
/** Long-term memory scope for this agent (same shape as {@link TaskSpec.memory}). */
memory?: TaskSpec["memory"];
/**
* This agent's own {@link TaskSpec.memoryPersistenceCapable} declaration — the same three-state
* statement, made per definition: an agent whose closure tools persist memory declares `true`
* (suppressing the read-only-memory disclosure no roster inference can clear), one whose roster
* looks write-capable but reaches no durable store declares `false` (forcing the disclosure).
* Arbitration with the spawning run's declaration: a parent's explicit `false` is a FLOOR and wins
* over a definition `true` (the disclosure is about the deployment's storage, which choosing this
* agent does not change); otherwise a declared definition value wins over the parent's; absent
* both, the child spec carries no value and the child's own roster inference decides.
* On the workflow lane (`agent(…, {agentType})`) the definition value fills an ABSENT governed-spec
* value only — a base/spec declaration of either polarity wins (the lane's spec-pinned-fields rule).
*/
memoryPersistenceCapable?: boolean;
/**
* Observer agents (CC 2.1.206 parity — 逐字锚 docs/CC206-OBSERVER-ANCHORS-2026-07-11.md, schema
* 面 C @28385708): "Agent type auto-spawned as a background observer whenever this agent runs.
* The observer receives read-only activity digests and reports via the ObserverReport tool; it
* never participates in the task." Resolution is warn-and-unobserved on an unknown type and
* refuses chaining (an observer's own `observer` declaration is ignored). Gated by
* {@link RuntimeCaps.allowObservers} — EXPLICIT opt-in, default OFF (the sema seat of CC's
* default-unset env `CLAUDE_CODE_EXPERIMENTAL_OBSERVER_AGENTS` + default-true statsig pair).
*/
observer?: string;
/**
* CC 面 C: "Supplemental postamble appended (after the harness-owned default) to each activity
* digest sent to the observer. Blank values are ignored."
*/
observerMessage?: string;
}
/** A skill made available to this task (passed as an object; not loaded from disk). */
export interface SkillSpec {
name: string;
description: string;
content: string;
/**
* The tools this skill lets run WITHOUT ASKING for the rest of the run it is activated in — the frontmatter
* `allowed-tools` list, spelled as a permission rule is spelled: a tool name (`Read`, `mcp__srv__get_issue`), a
* protocol namespace (`mcp__srv` / `mcp__srv__*` / `mcp__srv__get_*`), a shell command rule (`Bash(git:*)`) or the Read
* directory form (`Read(//abs/dir/**)`). The list is an allow-rule source, not a fence: an unlisted tool is judged as
* it always is, and a listed one is released only from the questions an allow rule may clear (a deny or ask rule, the
* plan mode, the organization ceiling and every mandated question still stand). Activating a skill that declares
* either list asks once (`Execute skill: <name>`, the card listing what it releases); a skill the person invoked by
* name ({@link import("./task-spec.js").TaskSpec.userInvokedSkills}) grants without asking. The grant holds for that
* run (a steer it takes in does not end it) and the next runTask starts without it. An entry this grammar reads as nothing — `*` (it voids an allow list; on a deny list it is inert and the rest of the
* list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`).
*/
allowedTools?: string[];
/** The tools refused for the rest of the run this skill is activated in — the frontmatter `disallowed-tools` list,
* in the {@link allowedTools} grammar (a path tool also takes a path pattern here); read wherever a person's deny
* rows are read, ahead of every allow. */
disallowedTools?: string[];
/**
* Optional attachments bundled with the skill (additive) — the CC bundled-skill shape (a SKILL.md whose
* body references sibling files by relative path) translated to sema's brain-outside posture, where the
* ruling is: **core never writes to disk.** CC extracts a bundle and lets the body reference files on
* the filesystem; sema core has no filesystem authority of its own, so materializing attachments into a
* working directory (making `Read`-by-path work) is the DEPLOYMENT's half. What core owns is exactly two
* faces, both bounded:
* 1. **disclosure** — each attachment's relative `path` is listed on the skill's `<skills>`-block line
* (metadata only, capped like the rest of the block), so the model knows the material exists before
* it loads the skill;
* 2. **delivery** — a `Skill` invoke appends an `## Attachments` section after the body (which is
* itself delivered untruncated): each file as `### <path>` + its content, under a SEPARATE total
* cap (`SKILL_ATTACHMENTS_MAX_CHARS`, 50k chars — an attachments-face budget; truncation/omission
* is always disclosed in-band). Without deployment-side materialization this inline delivery is the
* attachment's ONLY reachable face — that is the deliberate minimal shape, not an oversight.
*
* `path` is a relative, display-oriented identifier (how the body refers to the file); core never
* resolves it against a filesystem. `content` carries the SAME trust contract as {@link content}: skill
* material is CALLER-trusted instructions by design (see the skills module note) — a caller forwarding
* end-user skills is responsible for vetting attachments exactly like the body.
*/
files?: Array<{
path: string;
content: string;
}>;
/**
* The skill's own DIRECTORY on the machine the tools run on, so the model can reach its bundled material
* by path instead of searching the workspace. Core does not resolve it, read it, or check that it exists
* (the brain-outside posture {@link files} states). Core does exactly two textual things with it: the
* result opens with `Base directory for this skill: <baseDir>`, and both root placeholder spellings
* (`${SKILL_ROOT}`, `${CLAUDE_PLUGIN_ROOT}`) are substituted for it in the body and in every attachment's
* content, with `files[].path` then read RELATIVE to it. Absent ⇒ no line, no substitution, a
* byte-identical result; a placeholder with no directory stays verbatim.
*/
baseDir?: string;
}