UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

162 lines (161 loc) • 11.5 kB
/** * 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; }