@taicho-ai/contracts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # @taicho-ai/contracts
2
+
3
+ Runtime schemas and shared domain types for Taicho libraries. This package owns no I/O, model,
4
+ telemetry, or UI behavior.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@taicho-ai/contracts",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./*": "./src/*.ts"
8
+ },
9
+ "scripts": {
10
+ "build": "bun build src/index.ts --outdir dist --target bun",
11
+ "test": "bun test src"
12
+ },
13
+ "dependencies": {
14
+ "zod": "^4.4.3"
15
+ },
16
+ "description": "Shared runtime schemas and domain types for taicho (agents, briefs, traces, artifacts, teams).",
17
+ "license": "MIT",
18
+ "homepage": "https://taicho.ai",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/taicho-ai/taicho.git",
22
+ "directory": "packages/contracts"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "!src/**/*.test.ts",
27
+ "!src/**/*.test.tsx"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_TEAM_ID } from "./team";
3
+
4
+ /** Agent identity — canonical form lives in agents/<id>/agent.md frontmatter. */
5
+ const AgentObject = z.object({
6
+ id: z.string().regex(/^[a-z][a-z0-9-]*$/),
7
+ role: z.string(), // one-line capability description (shown in discovery)
8
+ identity: z.string(), // body of agent.md — the SOUL
9
+ // Capability allowlist. Built-in tool names (e.g. "delegate_task", "run_command") grant those
10
+ // tools. MCP capabilities are opt-in the SAME way (Plan 08 security hardening): "mcp:<server>"
11
+ // grants every tool that connected server exposes; "mcp:<server>/<tool>" grants exactly one.
12
+ // Anything NOT listed is not exposed to the agent — MCP tools included (no blanket grant).
13
+ tools: z.array(z.string()).default([]),
14
+ // Plan 22: the teams this agent sits on (was a single `team`). An agent may belong to MANY teams and
15
+ // declares them HERE — its own frontmatter is the single source of truth for membership, so
16
+ // teams/<id>/team.md deliberately carries no member list. This holds only EXPLICIT memberships: the
17
+ // universal `default` team is implicit (every agent belongs; the derived index adds it), so an empty
18
+ // list ⇒ default-only. A pre-Plan-22 `team: news` is migrated to `teams: [news]` at parse (below).
19
+ teams: z.array(z.string()).default([]),
20
+ // Visibility + delegation ACLs. Each entry is "*", an exact agent id, or (Plan 19) "team:<id>",
21
+ // which matches every member of that team. Additive: no existing agent id contains a colon.
22
+ canSee: z.array(z.string()).default(["*"]), // org visibility ACL
23
+ canDelegateTo: z.array(z.string()).default(["*"]),
24
+ budgets: z.object({
25
+ maxIterationsPerRun: z.number().int().positive().default(30),
26
+ maxWorkItemsPerRequest: z.number().int().positive().default(20),
27
+ maxTokensPerRun: z.number().int().positive().optional(),
28
+ maxCostPerRunUsd: z.number().positive().optional(),
29
+ // Plan 04 Phase 3: how many background tasks this agent may have RUNNING at once (config
30
+ // disposes the concurrency the model proposes via dispatch_task). Undefined ⇒ unbounded;
31
+ // extra dispatches sit in the persistent queue (status "queued") until a slot frees.
32
+ maxConcurrentRuns: z.number().int().positive().optional(),
33
+ }).prefault({}),
34
+ isRoot: z.boolean().default(false),
35
+ created: z.string().datetime(),
36
+ });
37
+
38
+ /** Parse an agent, migrating the legacy single `team: <id>` into `teams: [<id>]` so a pre-Plan-22
39
+ * agent.md still loads (files are canon; boot reindex then re-serializes it in the new shape). */
40
+ export const AgentDef = z.preprocess((raw) => {
41
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
42
+ const o = raw as Record<string, unknown>;
43
+ if (o.teams === undefined && "team" in o)
44
+ o.teams = o.team == null || o.team === "" ? [] : [o.team];
45
+ if ("team" in o) delete o.team;
46
+ }
47
+ return raw;
48
+ }, AgentObject);
49
+ export type AgentDef = z.infer<typeof AgentDef>;
50
+
51
+ /** An agent's EFFECTIVE membership: the universal `default` team plus its explicit teams, deduped, with
52
+ * default FIRST. This is what the ACL matches against and what the derived agent_teams index stores, so
53
+ * `team:default` matches everyone and membersOf("default") is the whole squad. Kept out of the file —
54
+ * agent.md lists only explicit teams — so a squad with no explicit teams looks byte-identical to before. */
55
+ export function effectiveTeams(agent: { teams: string[] }): string[] {
56
+ const out = [DEFAULT_TEAM_ID];
57
+ for (const t of agent.teams) if (t !== DEFAULT_TEAM_ID && !out.includes(t)) out.push(t);
58
+ return out;
59
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { VerificationVerdict } from "./trace";
3
+
4
+ /** Feedback attached to a SPECIFIC artifact version. Because the store is immutable-per-version, an
5
+ * annotation always pins to a concrete handle ("id@vN") — the feedback is anchored to the exact bytes
6
+ * it was written about. The author may be the human captain, a producing/reviewing agent, or the
7
+ * delegation checker (Plan 06): a verification verdict is an annotation like any other — it rides in
8
+ * the optional `verdict` field, so machine feedback and human feedback converge on ONE shape and ONE
9
+ * revision path.
10
+ *
11
+ * An OPEN annotation is the input to a REVISION run: run.ts surfaces open annotations under each input
12
+ * artifact handed to a child, the reviser addresses them and saves a NEW version linked back via
13
+ * `parents`, and (optionally) resolves the annotation against that revision. Mirrors schemas/policy.ts
14
+ * (a small typed record with a stable id + status lifecycle). */
15
+ export const AnnotationKind = z.enum(["feedback", "verification", "approval"]);
16
+ export type AnnotationKind = z.infer<typeof AnnotationKind>;
17
+
18
+ export const AnnotationStatus = z.enum(["open", "addressed", "dismissed"]);
19
+ export type AnnotationStatus = z.infer<typeof AnnotationStatus>;
20
+
21
+ export const Annotation = z.object({
22
+ id: z.string(), // ann_xxxx
23
+ target: z.string(), // the artifact handle this annotates — always "id@vN" (a concrete version)
24
+ author: z.string(), // "human" | agentId | "checker" — provenance of the feedback
25
+ kind: AnnotationKind.default("feedback"),
26
+ body: z.string(), // the feedback text (for a verdict: the reasons, human-readable)
27
+ verdict: VerificationVerdict.optional(), // Plan 06 hook: a verification verdict IS an annotation
28
+ status: AnnotationStatus.default("open"),
29
+ resolvedBy: z.string().optional(), // the revision handle "id@vN" that addressed this
30
+ created: z.string().datetime(),
31
+ });
32
+ export type Annotation = z.infer<typeof Annotation>;
33
+
34
+ /** A short, collision-resistant annotation id. Mirrors coaching's `pol_`/`ex_` id shape. */
35
+ export function mkAnnotationId(): string {
36
+ return `ann_${crypto.randomUUID().slice(0, 8)}`;
37
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+
3
+ /** Where an artifact's bytes live. PAYLOAD-AGNOSTIC — the store never interprets the body:
4
+ * - `file` — local bytes on disk under artifacts/<id>/ (path is absolute).
5
+ * - `external` — a locator into a system an MCP server fronts (a Notion page, a ClickUp task, a
6
+ * URL). Forcing a copy into artifacts/ would be wrong there; the envelope (provenance, versioning,
7
+ * summary, handle-based hand-off) works identically either way. See reference §5b. */
8
+ export const ArtifactLocation = z.discriminatedUnion("kind", [
9
+ z.object({ kind: z.literal("file"), path: z.string() }),
10
+ z.object({ kind: z.literal("external"), uri: z.string() }),
11
+ ]);
12
+ export type ArtifactLocation = z.infer<typeof ArtifactLocation>;
13
+
14
+ /** A structured hand-off artifact — the ENVELOPE only; the body is opaque bytes (or an external
15
+ * ref). Addressable by a stable logical `id` (a slug shared across versions), versioned and
16
+ * IMMUTABLE-PER-VERSION (a revision is a new version, never an overwrite). Provenance (producer
17
+ * agent + run) and lineage (`parents`) give a hand-off graph. Mirrors schemas/knowledge.ts. */
18
+ export const Artifact = z.object({
19
+ id: z.string(), // stable logical id (slug), shared across versions
20
+ version: z.number().int().positive().default(1), // increments per revision; (id,version) is immutable
21
+ title: z.string(),
22
+ type: z.string().default("document"), // FREE-FORM tag, never an enforced taxonomy
23
+ role: z.enum(["output", "input", "resource"]).default("output"), // §3c: one store, role-tagged
24
+ producer: z.string(), // agentId that produced it (provenance)
25
+ runId: z.string(), // run that produced it (provenance)
26
+ parents: z.array(z.string()).default([]), // parent artifact handles (lineage)
27
+ summary: z.string().optional(), // summary-first read; never the whole body
28
+ location: ArtifactLocation, // local file OR external ref (payload-agnostic)
29
+ created: z.string().datetime(),
30
+ });
31
+ export type Artifact = z.infer<typeof Artifact>;
32
+
33
+ /** Parse a handle: "id" (⇒ latest version) or "id@vN" (⇒ that version). Malformed suffixes fall
34
+ * back to treating the whole thing as an id (no version), so a stray "@" never throws. */
35
+ export function parseHandle(handle: string): { id: string; version?: number } {
36
+ const at = handle.lastIndexOf("@v");
37
+ if (at <= 0) return { id: handle };
38
+ const v = Number(handle.slice(at + 2));
39
+ if (!Number.isInteger(v) || v <= 0) return { id: handle };
40
+ return { id: handle.slice(0, at), version: v };
41
+ }
42
+
43
+ /** The canonical addressable handle for a specific version: "id@vN". */
44
+ export function artifactHandle(a: { id: string; version: number }): string {
45
+ return `${a.id}@v${a.version}`;
46
+ }
package/src/brief.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+
3
+ /** Delegation brief — sender writes goal+context; receiver supplies its own identity & policies. */
4
+ export const Brief = z.object({
5
+ to: z.string(),
6
+ goal: z.string(),
7
+ context: z.string().optional(),
8
+ criteria: z.string().optional(), // acceptance criteria the output must meet (rides into the prompt + drives the checker)
9
+ from: z.string(), // sending agent id
10
+ fromRun: z.string(),
11
+ });
12
+ export type Brief = z.infer<typeof Brief>;
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export * from "./agent";
2
+ export * from "./annotation";
3
+ export * from "./artifact";
4
+ export * from "./brief";
5
+ export * from "./knowledge";
6
+ export * from "./plan";
7
+ export * from "./policy";
8
+ export * from "./schedule";
9
+ export * from "./skill";
10
+ export * from "./team";
11
+ export * from "./trace";
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+
3
+ /** A typed, directed edge from one knowledge node to another (stored in the source node's frontmatter). */
4
+ export const KbEdge = z.object({
5
+ to: z.string(), // target node id
6
+ rel: z.string().default("relates_to"), // relates_to|depends_on|part_of|contradicts|derived_from|<free>
7
+ weight: z.number().default(1),
8
+ metadata: z.record(z.string(), z.unknown()).optional(),
9
+ });
10
+ export type KbEdge = z.infer<typeof KbEdge>;
11
+
12
+ /** Plan 19: the squad-wide scope was spelled "deck" through Plan 18. Accept the legacy value when
13
+ * parsing an existing kb/nodes/*.md and normalize it to "squad", so a workspace written by an older
14
+ * taicho still loads. `reconcileKbScope` (store/knowledge.ts, called at boot) rewrites those files
15
+ * once; this preprocess is what keeps them readable until it does. Ph8 adds "team" to the enum. */
16
+ export const KbScope = z
17
+ .preprocess((v) => (v === "deck" ? "squad" : v), z.enum(["squad"]))
18
+ .default("squad");
19
+
20
+ /** A squad-shared knowledge node — one file per node at kb/nodes/<kb_id>.md
21
+ * (YAML frontmatter = the node minus `content`, body = the content). Mirrors schemas/policy.ts. */
22
+ export const KbNode = z.object({
23
+ id: z.string(), // kb_xxxx
24
+ kind: z.string().default("fact"), // fact|entity|decision|doc|... (open vocab)
25
+ title: z.string(),
26
+ summary: z.string().optional(),
27
+ content: z.string(), // body of the .md
28
+ source: z.string().optional(), // provenance: "agentId:runId" / url
29
+ scope: KbScope,
30
+ edges: z.array(KbEdge).default([]), // outgoing typed edges
31
+ metadata: z.record(z.string(), z.unknown()).optional(),
32
+ created: z.string().datetime(),
33
+ updated: z.string().datetime().optional(),
34
+ });
35
+ export type KbNode = z.infer<typeof KbNode>;
package/src/plan.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { z } from "zod";
2
+
3
+ /** Plan 18. A plan separates STRUCTURE from STATE, because they answer different questions and change
4
+ * at wildly different rates:
5
+ *
6
+ * · a plan VERSION (v1, v2, …) is an immutable snapshot of the item SET — the intent. Minted only
7
+ * when the shape changes: an item added, removed, reworded, reordered. That is a replan, and it
8
+ * deserves a version. (This is store/artifacts.ts.)
9
+ * · an item TRANSITION is an append-only line in plans/<id>/events.jsonl. Ticking a box appends; it
10
+ * never mints a version. (This is store/annotations.ts.)
11
+ *
12
+ * Current state is fold(events) over the latest version — the same ledger-is-truth, cache-is-derived
13
+ * discipline that already governs ledger.jsonl → thread.jsonl. Had every tick minted a version, a
14
+ * twelve-item plan with three transitions each would produce thirty-six versions of an object whose
15
+ * shape never changed, and "when did this agent change its mind?" would be buried under bookkeeping. */
16
+
17
+ export const PlanItemStatus = z.enum([
18
+ "pending",
19
+ "in_progress",
20
+ "done",
21
+ "failed",
22
+ "blocked",
23
+ "interrupted", // the process died while this item's bound run was in flight (boot reconcile)
24
+ "dropped", // the agent abandoned it on purpose; requires a note
25
+ ]);
26
+ export type PlanItemStatus = z.infer<typeof PlanItemStatus>;
27
+
28
+ /** Terminal for the purposes of "is this item still open". `dropped` counts as settled. */
29
+ export const TERMINAL_ITEM_STATUS: ReadonlySet<PlanItemStatus> = new Set<PlanItemStatus>([
30
+ "done", "failed", "blocked", "interrupted", "dropped",
31
+ ]);
32
+
33
+ /** An item id is stable ACROSS versions. That is what lets an event logged against v1 still resolve
34
+ * after a replan mints v2 — the fold matches on item id, not on position. */
35
+ export const PlanItem = z.object({
36
+ id: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, "lowercase, digits, underscore, hyphen"),
37
+ text: z.string(),
38
+ assignee: z.string().optional(), // the agent or team this item is meant for
39
+ });
40
+ export type PlanItem = z.infer<typeof PlanItem>;
41
+
42
+ export const Plan = z.object({
43
+ id: z.string(), // p_<slug>
44
+ version: z.number().int().positive(),
45
+ owner: z.string(), // the agent whose plan this is
46
+ goal: z.string(),
47
+ items: z.array(PlanItem),
48
+ parents: z.array(z.string()).default([]), // lineage: ["p_ship@v1"] — a replan's provenance
49
+ producer: z.string(),
50
+ runId: z.string(),
51
+ created: z.string().datetime(),
52
+ });
53
+ export type Plan = z.infer<typeof Plan>;
54
+
55
+ /** One line per transition. The LATEST line for an item wins the fold. */
56
+ export const PlanEvent = z.object({
57
+ item: z.string(),
58
+ status: PlanItemStatus,
59
+ /** Who wrote it. An `engine` event is the truth about what a run actually did; a `model` event is a
60
+ * claim. Both are recorded — a model trying to mark a failed delegation `done` is exactly the
61
+ * behaviour you want visible in a trace, not silently swallowed. */
62
+ by: z.enum(["model", "engine"]),
63
+ runId: z.string(), // the run that wrote the event
64
+ /** The child run (or background taskId) this item is bound to. Once set, ONLY the engine may set the
65
+ * item's terminal status: the checkbox reflects what happened, not what the model claims. */
66
+ boundRunId: z.string().optional(),
67
+ note: z.string().optional(), // e.g. a failed verdict's reasons
68
+ /** A recorded-but-refused attempt: the model tried to set a terminal status on an engine-owned item.
69
+ * The fold SKIPS these — otherwise the attempt would be the last line for that item and would win,
70
+ * which is precisely the lie the engine-owns rule exists to prevent. It stays in the log because a
71
+ * model marking a failed delegation `done` is a fact worth having. */
72
+ rejected: z.boolean().optional(),
73
+ ts: z.string().datetime(),
74
+ });
75
+ export type PlanEvent = z.infer<typeof PlanEvent>;
76
+
77
+ /** An item with its folded state — what the panel renders and what the model is shown. */
78
+ export interface FoldedItem extends PlanItem {
79
+ status: PlanItemStatus;
80
+ boundRunId?: string;
81
+ note?: string;
82
+ /** When this item last changed, for elapsed-in-state. */
83
+ updated?: string;
84
+ }
85
+
86
+ export interface PlanState {
87
+ plan: Plan;
88
+ handle: string;
89
+ items: FoldedItem[];
90
+ counts: { total: number; done: number; open: number; failed: number };
91
+ }
92
+
93
+ export const planHandle = (p: { id: string; version: number }): string => `${p.id}@v${p.version}`;
94
+
95
+ /** `p_ship` (latest) or `p_ship@v2` (a concrete version). Mirrors schemas/artifact.ts parseHandle. */
96
+ export function parsePlanHandle(handle: string): { id: string; version?: number } {
97
+ const at = handle.lastIndexOf("@v");
98
+ if (at < 0) return { id: handle };
99
+ const v = Number(handle.slice(at + 2));
100
+ return Number.isInteger(v) && v > 0 ? { id: handle.slice(0, at), version: v } : { id: handle };
101
+ }
102
+
103
+ /** A stable id from a goal. Deterministic, so re-stating the same goal continues the same plan rather
104
+ * than forking a second one the agent then has to reconcile against. */
105
+ export function planIdForGoal(goal: string): string {
106
+ const slug = goal
107
+ .toLowerCase()
108
+ .replace(/[^a-z0-9]+/g, "-")
109
+ .replace(/^-+|-+$/g, "")
110
+ .slice(0, 40)
111
+ .replace(/-+$/g, "");
112
+ return `p_${slug || "plan"}`;
113
+ }
package/src/policy.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+
3
+ /** A coaching note — one file per note under agents/<id>/policies/. */
4
+ export const PolicyNote = z.object({
5
+ id: z.string(), // pol_xxxx
6
+ agent: z.string(),
7
+ when: z.string(), // condition, enforced at retrieval
8
+ do: z.string(), // instruction
9
+ scope: z.enum(["agent", "global"]).default("agent"),
10
+ status: z.enum(["proposed", "approved", "rejected", "superseded"]),
11
+ supersedes: z.string().optional(), // id of the note this replaces
12
+ taughtBy: z.string(),
13
+ fromRun: z.string().optional(), // run id the coaching came from
14
+ created: z.string().datetime(),
15
+ expanded: z.array(z.string()).default([]), // write-time paraphrases for recall
16
+ });
17
+ export type PolicyNote = z.infer<typeof PolicyNote>;
18
+
19
+ /** Approved output bound to a request pattern. */
20
+ export const Exemplar = z.object({
21
+ id: z.string(), // ex_xxxx
22
+ agent: z.string(),
23
+ pattern: z.string(), // request pattern this answers
24
+ artifact: z.string(), // artifact HANDLE ("id" ⇒ latest, or "id@vN") — resolves via the
25
+ // artifact store (store/artifacts.ts), NOT a raw filesystem path.
26
+ // Plan 01 Ph4c: keying on the stable, versioned, relocatable id
27
+ // (never an absolute path) is what lets an exemplar survive a
28
+ // revision/workspace move — same addressing as delegate hand-off.
29
+ mode: z.enum(["serve", "imitate"]).default("imitate"),
30
+ status: z.enum(["proposed", "approved", "rejected", "superseded"]),
31
+ taughtBy: z.string(),
32
+ created: z.string().datetime(),
33
+ });
34
+ export type Exemplar = z.infer<typeof Exemplar>;
@@ -0,0 +1,55 @@
1
+ /** Plan 04 Phase 6 — a Schedule fires an UNATTENDED run on a schedule (cron-style) or a trigger
2
+ * (a file watch), driving the goal through the headless `executeRun` path (Plan 03's seam). A
3
+ * schedule persists to `schedules/<id>.json` (files are canon) so it survives restarts and is
4
+ * reconciled/armed on boot.
5
+ *
6
+ * APPROVALS: a scheduled run is unattended — there is no captain watching an approval card — so the
7
+ * approval channel is restricted to `reject` (the safe default: decline every privileged action) or
8
+ * `approve` (scripted/trusted only). `prompt` is meaningless with no stdin and is not allowed here. */
9
+ import { z } from "zod";
10
+
11
+ /** Unattended approval modes only. `reject` = the safe default (decline privileged tools); `approve`
12
+ * = trusted/scripted (approve everything). No `prompt` — nobody is there to answer. */
13
+ export const ScheduleApprove = z.enum(["reject", "approve"]);
14
+ export type ScheduleApprove = z.infer<typeof ScheduleApprove>;
15
+
16
+ /** What makes a schedule due:
17
+ * - `cron` — a 5-field cron expression, evaluated in **UTC** (deterministic; no DST ambiguity).
18
+ * - `interval` — fire every `everyMs` milliseconds.
19
+ * - `watch` — fire when the file/dir at `path` changes (its mtime moves). Polled on each tick. */
20
+ export const Trigger = z.discriminatedUnion("kind", [
21
+ z.object({ kind: z.literal("cron"), expr: z.string().min(1) }),
22
+ z.object({ kind: z.literal("interval"), everyMs: z.number().int().positive() }),
23
+ z.object({ kind: z.literal("watch"), path: z.string().min(1) }),
24
+ ]);
25
+ export type Trigger = z.infer<typeof Trigger>;
26
+
27
+ export const Schedule = z.object({
28
+ id: z.string().min(1),
29
+ goal: z.string().min(1),
30
+ agent: z.string().min(1).default("root"),
31
+ trigger: Trigger,
32
+ approve: ScheduleApprove.default("reject"),
33
+ enabled: z.boolean().default(true),
34
+ created: z.string(),
35
+ updated: z.string(),
36
+ // Mutable scheduling state, advanced by the runner on each fire and persisted so cadence survives
37
+ // a restart. `nextDueAt` is the next computed fire time (cron/interval); `lastMtimeMs` the last-seen
38
+ // mtime (watch). `lastRunId`/`lastStatus` record the outcome of the most recent fire.
39
+ runCount: z.number().int().nonnegative().default(0),
40
+ lastRunAt: z.string().optional(),
41
+ lastRunId: z.string().optional(),
42
+ lastStatus: z.string().optional(),
43
+ nextDueAt: z.string().optional(),
44
+ lastMtimeMs: z.number().optional(),
45
+ });
46
+ export type Schedule = z.infer<typeof Schedule>;
47
+
48
+ /** The fields a caller supplies to create a schedule (the rest are defaulted / assigned). */
49
+ export interface ScheduleSpec {
50
+ id?: string;
51
+ goal: string;
52
+ agent?: string;
53
+ trigger: Trigger;
54
+ approve?: ScheduleApprove;
55
+ }
package/src/skill.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+
3
+ /** A reusable procedure document — one file per skill at skills/<id>.md
4
+ * (YAML frontmatter = the skill minus `body`, body = the procedure). Mirrors schemas/policy.ts. */
5
+ export const Skill = z.object({
6
+ id: z.string(), // skill_xxxx
7
+ name: z.string(), // short, unique; used by use_skill + display
8
+ description: z.string(), // WHEN to use it — drives discovery + injection
9
+ tags: z.array(z.string()).default([]),
10
+ status: z.enum(["active", "draft"]).default("active"), // only `active` are injected/usable
11
+ body: z.string(), // the procedure (markdown)
12
+ created: z.string().datetime(),
13
+ updated: z.string().datetime().optional(),
14
+ });
15
+ export type Skill = z.infer<typeof Skill>;
package/src/team.ts ADDED
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+
3
+ /** Plan 22: the universal team. Every agent belongs to `default` — it IS the squad, expressed as a
4
+ * team. It is seeded on boot, cannot be deleted, and is IMPLICIT: an agent never has to list it in its
5
+ * own `teams:` frontmatter (the derived index adds it). Root leads it. Explicit teams (news, trading…)
6
+ * are additive overlays on top; joining one never removes an agent from default. */
7
+ export const DEFAULT_TEAM_ID = "default";
8
+
9
+ /** A team's tool policy, applied in toolsForAgent to every member.
10
+ * `grant` ADDS to a member's own toolset; `deny` REMOVES from it and wins over the member's own grant.
11
+ * A `deny` list that intersects DEFAULT_WORKER_TOOLS is rejected when the team loads (store/teams.ts):
12
+ * Plan 14's capability FLOOR — a worker can always produce and hand off an artifact — is not a team's
13
+ * to punch through. `grant` may name privileged tools; that is safe precisely because teams are
14
+ * captain-authored files and there is deliberately no create_team tool. */
15
+ export const TeamTools = z
16
+ .object({
17
+ grant: z.array(z.string()).default([]),
18
+ deny: z.array(z.string()).default([]),
19
+ })
20
+ .prefault({});
21
+ export type TeamTools = z.infer<typeof TeamTools>;
22
+
23
+ /** Plan 19: a team is a functional group within the squad — news, trading, programming.
24
+ *
25
+ * Canonical form is teams/<id>/team.md: YAML frontmatter (this schema minus `charterBody`) plus a
26
+ * markdown body that becomes the team's standing instruction, injected into every member's system
27
+ * prompt as a context-tier section. Mirrors agents/<id>/agent.md exactly.
28
+ *
29
+ * MEMBERSHIP IS NOT HERE. An agent declares `team: <id>` in its own frontmatter — one source of truth —
30
+ * and a team's roster is derived by grouping the registry. Listing members here too would let the two
31
+ * drift. `lead` names an agent that must itself declare this team; a mismatch is a boot error. */
32
+ export const TeamDef = z.object({
33
+ id: z.string().regex(/^[a-z][a-z0-9-]*$/),
34
+ /** One line, shown in root's roster in place of the members it stands for. */
35
+ charter: z.string(),
36
+ /** Optional. Present ⇒ delegate_task(to:"<id>") routes to this agent, which then delegates within
37
+ * its team; it costs one delegation level and one model call. Absent ⇒ the engine routes to the
38
+ * best-ranked member directly, for free. Leadless is the right default. */
39
+ lead: z.string().optional(),
40
+ tools: TeamTools,
41
+ /** Body of team.md — the team's standing instruction. Culture is configuration. */
42
+ charterBody: z.string().default(""),
43
+ created: z.string().datetime(),
44
+ });
45
+ export type TeamDef = z.infer<typeof TeamDef>;
46
+
47
+ /** A member's effective toolset: its own grant, minus the team's `deny`, plus the team's `grant`.
48
+ *
49
+ * `deny` wins over BOTH the member's own grant and the team's own `grant` — a team that lists a tool in
50
+ * both is unambiguous rather than order-dependent. The DEFAULT_WORKER_TOOLS floor is protected earlier,
51
+ * when the team file loads (store/teams.ts assertPolicyRespectsFloor), so nothing here can strip a
52
+ * worker's ability to produce and hand off an artifact. */
53
+ export function effectiveTools(agentTools: string[], policy?: TeamTools): string[] {
54
+ if (!policy || (!policy.grant.length && !policy.deny.length)) return agentTools;
55
+ const denied = new Set(policy.deny);
56
+ const out: string[] = [];
57
+ const seen = new Set<string>();
58
+ for (const t of [...agentTools, ...policy.grant])
59
+ if (!denied.has(t) && !seen.has(t)) { seen.add(t); out.push(t); }
60
+ return out;
61
+ }
62
+
63
+ /** Fold several teams' tool policies into one (Plan 22: an agent may sit on many teams). Grants union,
64
+ * denies union — and since effectiveTools lets deny win over grant, a tool ANY of the agent's teams
65
+ * denies is denied for it, even if another grants it. The DEFAULT_WORKER_TOOLS floor is protected
66
+ * per-team at load (assertPolicyRespectsFloor), so the union can never strip a worker's artifact tools. */
67
+ export function mergeTeamPolicies(policies: (TeamTools | undefined)[]): TeamTools {
68
+ const grant: string[] = [];
69
+ const deny: string[] = [];
70
+ for (const p of policies) {
71
+ if (!p) continue;
72
+ for (const g of p.grant) if (!grant.includes(g)) grant.push(g);
73
+ for (const d of p.deny) if (!deny.includes(d)) deny.push(d);
74
+ }
75
+ return { grant, deny };
76
+ }
77
+
78
+ /** The ACL grammar's team production (core/registry.ts). An entry in canSee/canDelegateTo is `"*"`,
79
+ * an exact agent id, or `team:<id>`. No existing id contains a colon, so this is purely additive. */
80
+ export const TEAM_ACL_PREFIX = "team:";
81
+
82
+ export const teamAclEntry = (teamId: string): string => `${TEAM_ACL_PREFIX}${teamId}`;
83
+
84
+ /** The team named by an ACL entry, or null when the entry is `"*"` or an exact agent id. */
85
+ export function parseTeamAcl(entry: string): string | null {
86
+ return entry.startsWith(TEAM_ACL_PREFIX) ? entry.slice(TEAM_ACL_PREFIX.length) : null;
87
+ }
package/src/trace.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+
3
+ export const CoachingLedger = z.object({
4
+ retrieved: z.array(z.string()), // policy ids
5
+ applied: z.array(z.string()),
6
+ skipped: z.array(z.object({ id: z.string(), reason: z.string() })),
7
+ knowledge: z.array(z.string()).default([]), // kb node ids injected into context (default keeps old traces parseable)
8
+ skills: z.array(z.string()).default([]), // skill ids injected into context (default keeps old traces parseable)
9
+ });
10
+
11
+ /** An independent checker's judgement of a delegated output against its acceptance criteria. */
12
+ export const VerificationVerdict = z.object({
13
+ pass: z.boolean(),
14
+ reasons: z.array(z.string()).default([]), // when pass=false, the unmet criteria; may be empty on pass
15
+ });
16
+ export type VerificationVerdict = z.infer<typeof VerificationVerdict>;
17
+
18
+ /** One criteria→verdict record for a delegation that carried acceptance criteria. A failed initial
19
+ * verdict followed by a retry yields two records (retried:false, then retried:true). */
20
+ export const VerificationRecord = z.object({
21
+ criteria: z.string(),
22
+ verdict: VerificationVerdict,
23
+ runId: z.string(), // the child run whose output was checked
24
+ retried: z.boolean().default(false),
25
+ tokens: z.number().default(0), // the checker call's own spend (advisory; not a hard budget)
26
+ costUsd: z.number().nullable().default(0), // null on a subscription checker (no measurable USD) — never a fabricated 0
27
+ costNote: z.string().optional(), // "subscription" when the checker ran on an unpriced subscription model
28
+ checkerError: z.boolean().optional(), // Plan 20: the checker itself never ran (outage/cancel) — the verdict means "unverified", not a judged fail
29
+ });
30
+ export type VerificationRecord = z.infer<typeof VerificationRecord>;
31
+
32
+ /** One file per run under runs/<agent>/. Written by agents, read by humans + coaching flow only. */
33
+ export const RunTrace = z.object({
34
+ id: z.string(), // <agent>/<date>-run<n>
35
+ agent: z.string(),
36
+ task: z.string(),
37
+ triggeredBy: z.string(), // "user" | run id of delegating run
38
+ ledger: CoachingLedger,
39
+ toolCalls: z.array(z.object({ tool: z.string(), count: z.number() })),
40
+ artifacts: z.array(z.string()), // artifact references (handles/paths) THIS run produced
41
+ // Hand-off graph (parent-side, mirrors delegatedOut): what flowed across this run's delegation
42
+ // edges. inputArtifacts = handles handed DOWN to children; outputArtifacts = handles received UP.
43
+ // Defaults keep pre-Plan-01 traces parseable.
44
+ inputArtifacts: z.array(z.string()).default([]),
45
+ outputArtifacts: z.array(z.string()).default([]),
46
+ delegatedOut: z.array(z.string()), // run ids
47
+ verification: z.array(VerificationRecord).default([]), // criteria→verdict records for this run's delegations (default keeps old traces parseable)
48
+ outcome: z.enum(["completed", "blocked", "failed", "interrupted"]),
49
+ tokens: z.number().default(0),
50
+ // Plan 05: peak estimated context size (system + messages, chars/4) actually sent to the model this
51
+ // run — post-compaction. Advisory (a gate estimate, not billed tokens). Default keeps old traces parseable.
52
+ contextTokens: z.number().default(0),
53
+ costUsd: z.number().nullable().default(0),
54
+ costNote: z.string().optional(),
55
+ // This run's OWN delegation-checker (Plan 06) spend, kept separate from `tokens`/`costUsd` (the
56
+ // primary loop) because the verifier call creates NO child trace. /costs adds these to the run's
57
+ // own-spend sum so verifier tokens/USD surface exactly ONCE (no child double-count). USD is 0 for a
58
+ // subscription/unmeasurable run (mirrors costUsd:null there) — never a fabricated price. Defaults
59
+ // keep pre-Plan-09 traces parseable.
60
+ verifierTokens: z.number().default(0),
61
+ verifierCostUsd: z.number().default(0),
62
+ model: z.string().optional(), // resolved model id — the /costs "by provider" dimension (default keeps old traces parseable)
63
+ aggregate: z.object({ tokens: z.number(), costUsd: z.number().nullable() }).optional(),
64
+ // Plan 18: the agent's plan handle at run end, and how many transitions this run wrote. Both OPTIONAL
65
+ // (not .default(0)) so every trace written before this plan parses AND so the hand-built RunTrace
66
+ // fixtures across the suite need no edit — absent simply means "this run had no plan".
67
+ plan: z.string().optional(),
68
+ planEvents: z.number().optional(),
69
+ notes: z.array(z.string()).default([]),
70
+ durationMs: z.number().default(0),
71
+ started: z.string().datetime(),
72
+ });
73
+ export type RunTrace = z.infer<typeof RunTrace>;