UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

124 lines (123 loc) • 9.57 kB
/** * PARITY-SPOT-WORKFLOW B5/F2 (CC pretty.js:449105-449141 + `Pqi` :149631) — the script-persistence seam behind * the Workflow tool's `scriptPath`/`name` calling surface. CC persists EVERY invocation's script under the * session directory and returns the path in the tool result, so the model iterates by EDITING THE FILE and * re-invoking with `{scriptPath}` instead of resending the whole script; `{name}` resolves a saved workflow. * * Core is deployment-neutral: the tool takes this seam via `RunWorkflowToolDeps.scriptStore` (optional — when * absent, `scriptPath`/`name` calls get an honest structured error and inline `script` works as before). The * file-backed implementation below is the TOC default (mirrors CC's session-dir posture: dir 0o700 / file * 0o600, best-effort persist that never blocks a launch). */ /** * design/140 §6 1a — a named-registry entry's RESOLVED form: the script source plus the registration's * `defaultArgs` layer (the middle tier of the merge chain: call-time args > registered defaultArgs > * script-internal fallbacks). ONE return shape: a registry that carries no defaults returns `{ script }` * — the bare-string return is RETIRED (RB-482 #9, ruled 2026-08-03), matching {@link * WorkflowScriptStore.resolveName}'s contract. A plain-JS store still returning a bare string is refused * with a structured migration error at the tool boundary, not read as a resolution. */ export interface NamedWorkflowResolution { script: string; /** Registration-time default args (a JSON value — this is where a control-plane/center下发的默认 lands). * Merged UNDER the call-time args by {@link mergeWorkflowArgs} at tool-resolve time (parse-time snapshot: * the run receives the merged VALUE; nothing re-queries the registry after launch). */ defaultArgs?: unknown; /** * CORE-1 — the top-level key a BARE STRING call-time arg normalizes into, so the "裸 string = * <key>" ergonomic entry (published by the built-in `discussion` script: a bare string args IS the * topic) composes with a registered object `defaultArgs` instead of colliding with it. When a `{name}` call * passes a raw string AND this resolution carries an object `defaultArgs`, the tool wraps the string as * `{ [stringArgKey]: <string> }` BEFORE the object merge (see {@link normalizeStringArg} / the run-workflow * tool's resolve path). Defaults to `"topic"` (the built-in ergonomic contract). Ignored when `defaultArgs` * is not a plain object (a bare string then reaches the script verbatim — pre- behavior preserved). */ stringArgKey?: string; } /** * design/140 §6 1b — one row of the named-workflow LISTING projection (the LLM-facing consumption face of * `meta.whenToUse`). Mirrors the agents-side roster entry (subagent.ts `agentWhenToUseText`/CC `tIl`): * `whenToUse` is shown when present, else `description`. */ export interface NamedWorkflowListing { name: string; description?: string; whenToUse?: string; } export interface WorkflowScriptStore { /** B-4 — the CONFORMANCE MARKER for the scope-partitioned contract. Structural * typing would otherwise let a pre-partition adapter (`load(path)`, `persist(runId, script)`) * remain assignable — fewer-parameter functions are compatible in TypeScript — and silently * ignore the scope argument, serving cross-tenant reads while the tool presents the operation as * scope-bounded. A REQUIRED literal field breaks that assignability at compile time; the * run-workflow tool also refuses at runtime when it is absent (plain-JS adapters). */ readonly scopePartitioned: true; /** Persist THIS invocation's resolved script, keyed by its runId. Returns the persisted path (what the tool * reports back to the model). Failures should throw — the tool treats persistence as best-effort. * B-4 (BREAKING): `scope` PARTITIONS per-run scripts — the CC anchor is a per-session directory, * and the port had dropped the axis (one flat directory let a model-supplied scriptPath read another * scope's script). Implementations key storage by scope (the file store uses per-scope subdirs). */ persist(runId: string, script: string, scope: string): Promise<string> | string; /** Read a previously persisted script back. MUST refuse paths outside the store (the argument is * model-supplied — containment is the store's contract, not the tool's). * B-4 (BREAKING): `scope` bounds the read to the CALLER's partition — a path pointing into * another scope's partition must be refused exactly like a path outside the store. */ load(scriptPath: string, scope: string): Promise<string> | string; /** Resolve a SAVED workflow name to its registration (undefined = unknown name). Optional — a deployment * without a named registry simply omits it. RB-482 #9 (ruled 2026-08-03): ONE return form — a * {@link NamedWorkflowResolution} ({ script, defaultArgs? }); the legacy bare-string return is retired. A * deployment entry of the SAME NAME as a built-in workflow SHADOWS the built-in (design/140 §6 1c). * RULING: deliberately NO scope axis here — saved names are a DEPLOYMENT-level registry (the * built-ins' tier), shared across scopes like agent definitions; a multi-tenant deployment that wants * per-tenant registries mounts per-tenant store instances. * CONTRACT (stated 2026-08-16, C-R14): this is a STABLE, SIDE-EFFECT-FREE lookup. Resolving ONE call * may ask it more than once with different spellings of the same slot (a built-in's retired name and * its canonical one), and the tool card probes it independently of the execute path — so an * implementation that counts queries, rate-limits, or mutates state per call is out of contract and * will behave differently depending on how many spellings a name has. */ resolveName?(name: string): Promise<NamedWorkflowResolution | undefined> | NamedWorkflowResolution | undefined; /** * design/140 §6 1b — enumerate the registry's SAVED workflows for the listing projection (tool card / * roster faces). Optional: a store without a stable name registry omits it (the file store below does — * its directory mixes per-run persisted scripts with saved names, so enumeration would advertise every * past run as a "named workflow"). NOTE: only a SYNCHRONOUS return reaches the statically-built Workflow * tool card; an async store should precompute if it wants card visibility. */ list?(): NamedWorkflowListing[] | Promise<NamedWorkflowListing[]>; } /** * design/140 §6 1a — the default-args merge: **call-time args > registered defaultArgs** (the script's own * internal fallbacks remain the third tier, applied by the script itself). Semantics (locked by test): * - both sides plain objects → SHALLOW top-level merge (`{...defaults, ...call}`): a top-level key present in * the call args wins WHOLESALE — nested objects/arrays/scalars are replaced, never deep-merged; * - call args absent (`undefined`) → the registered defaults verbatim; * - call args a non-object (array / scalar / null) → the call args win wholesale (no structural mixing). * Pure value-in/value-out: the caller snapshots the RESULT into the run (parse-time discipline, design/140 §2 — * a resume replay never re-queries the registry; a post-launch registry change cannot reach an in-flight run). */ export declare function mergeWorkflowArgs(callArgs: unknown, defaultArgs: unknown): unknown; /** * CORE-1 — NORMALIZE a bare-string call-time arg into `{ [key]: <string> }` so the published * "裸 string = <key>" ergonomic entry (built-in `discussion`: bare string args IS the topic) COMPOSES * with a registered object `defaultArgs` instead of colliding with it. * * The bug it fixes: {@link mergeWorkflowArgs}'s non-object branch lets a bare string win WHOLESALE — so a * named workflow whose registration carries object `defaultArgs` (center 配的 members/rounds/finalizer) had * its whole defaults block silently wiped the moment a caller used the ergonomic `{name, args: "<topic>"}` * form, and the collab template quietly ran as a generic advocate/skeptic. Wrapping the string here lets the * subsequent object merge preserve the defaults AND honor the topic. * * Applied ONLY when BOTH hold (otherwise return `callArgs` untouched — pre- behavior byte-preserved): * - `callArgs` is a string (the ergonomic form), AND * - `defaultArgs` is a PLAIN OBJECT (there ARE object defaults to merge under; with no/scalar defaults a bare * string must still reach the script verbatim — the built-in's own `raw === "string"` fallback handles it). * `key` is the resolution's `stringArgKey` (a registry may declare which key its bare string maps to); the * caller passes the built-in default `"topic"` when the resolution omits it. */ export declare function normalizeStringArg(callArgs: unknown, defaultArgs: unknown, key?: string): unknown; /** * File-backed {@link WorkflowScriptStore}: per-run scripts land as `<dir>/<scopeDir>/<runId>.js` * (B-4: per-scope partition — the CC per-session-dir posture restored on the tenant axis); * `resolveName` reads `<dir>/<name>.js` at the ROOT (the deployment-level saved-name tier, see the * interface RULING — deliberately outside every scope partition). */ export declare function createFileWorkflowScriptStore(dir: string): WorkflowScriptStore;