@rowan-agent/agent 0.6.2 → 0.7.1

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 CHANGED
@@ -616,7 +616,7 @@ Two sources, merged by priority:
616
616
 
617
617
  ```yaml
618
618
  ---
619
- name: Code Review
619
+ name: review
620
620
  description: Review code for correctness and style
621
621
  tools: [read, bash]
622
622
  target: execute
@@ -625,15 +625,14 @@ target: execute
625
625
  Review the current implementation for bugs and style issues.
626
626
  ```
627
627
 
628
- **Extension-registered** — via `api.registerPhase()`. Same id overrides file-based phases.
628
+ **Extension-registered** — via `api.registerPhase()`. The phase name is used as its identity.
629
629
 
630
630
  ```ts
631
631
  import type { ExtensionAPI } from "@rowan-agent/agent";
632
632
 
633
633
  export default function myPlugin(api: ExtensionAPI) {
634
634
  api.registerPhase({
635
- id: "review",
636
- name: "Code Review",
635
+ name: "review",
637
636
  description: "Review code for correctness",
638
637
  tools: ["read", "bash"],
639
638
  async run(context, execution) {
@@ -648,8 +647,7 @@ export default function myPlugin(api: ExtensionAPI) {
648
647
 
649
648
  ```ts
650
649
  interface Phase {
651
- id: string;
652
- name: string;
650
+ name: string; // unique identity and display name
653
651
  description: string;
654
652
  tools?: string[]; // restrict tools (undefined = all)
655
653
  skills?: string[]; // restrict skills
@@ -674,7 +672,7 @@ interface PhaseContext {
674
672
 
675
673
  type PhaseOutput = {
676
674
  message: string;
677
- route: string; // "continue" | "stop" | <phase-id>
675
+ route: string; // "continue" | "stop" | <phase-name>
678
676
  payload?: unknown; // data passed to the next phase
679
677
  };
680
678
  ```
@@ -731,7 +729,7 @@ import type { ExtensionAPI } from "@rowan-agent/agent";
731
729
  export default function myPlugin(rowan: ExtensionAPI) {
732
730
  rowan.on("agent_start", (event) => { ... });
733
731
  rowan.registerTool({ name: "my_tool", description: "...", parameters: {...}, execute: async (args) => {...} });
734
- rowan.registerPhase({ id: "review", description: "...", run: async (ctx) => {...} });
732
+ rowan.registerPhase({ name: "review", description: "...", run: async (ctx) => {...} });
735
733
  rowan.events.emit("my-plugin:ready", {});
736
734
  }
737
735
  ```
@@ -775,7 +773,7 @@ type LoopMetrics = {
775
773
  | `Skill` | Loaded skill bundle |
776
774
  | `Phase` | Phase definition with content, execution, and routing config |
777
775
  | `PhaseContext` / `PhaseOutput` | Phase input and output |
778
- | `PhaseRegistry` | Map of phase ids to Phase objects plus entry phase id |
776
+ | `PhaseRegistry` | Map of phase names to Phase objects plus entry phase name |
779
777
  | `Outcome` | Terminal result with message and tool results |
780
778
  | `LoopMetrics` | Loop iteration, timing, and phase transition stats |
781
779
  | `SessionManagerProvider` | Session lifecycle seam used by the Runtime |
@@ -792,4 +790,4 @@ type LoopMetrics = {
792
790
 
793
791
  ## Version
794
792
 
795
- Current version: **0.6.0**
793
+ Current version: **0.7.1**
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ type RuntimeEventId = OpaqueId<"RuntimeEventId">;
30
30
  type LeaseId = OpaqueId<"LeaseId">;
31
31
  type RuntimeToolCallId = OpaqueId<"RuntimeToolCallId">;
32
32
  type AgentLifecycleState = "active" | "paused";
33
+ type SessionLifecycleState = "active" | "archived" | "deleting";
33
34
  type RuntimeMessageState = "queued" | "leased" | "acknowledged" | "dead_lettered";
34
35
  type AgentRunState = "queued" | "running" | "suspended" | "completed" | "failed" | "cancelled";
35
36
  /** Opaque execution checkpoint owned by the Agent loop and persisted by Runtime. */
@@ -46,6 +47,8 @@ type AgentRecord = {
46
47
  id: AgentId;
47
48
  sessionId: string;
48
49
  state: AgentLifecycleState;
50
+ lifecycleState: SessionLifecycleState;
51
+ parentSessionId?: string;
49
52
  createdAt: string;
50
53
  updatedAt: string;
51
54
  };
@@ -84,7 +87,7 @@ type AgentRunRecord = {
84
87
  createdAt: string;
85
88
  updatedAt: string;
86
89
  };
87
- type RuntimeEventKind = "agent_created" | "agent_paused" | "agent_resumed" | "message_enqueued" | "run_enqueued" | "run_leased" | "lease_expired" | "lease_recovered" | "run_suspended" | "run_retry_scheduled" | "run_completed" | "run_aborted" | "message_acknowledged" | "message_dead_lettered" | "tool_call_created" | "tool_call_started" | "tool_call_failed" | "tool_call_completed" | "tool_call_indeterminate";
90
+ type RuntimeEventKind = "agent_created" | "agent_paused" | "agent_resumed" | "session_archived" | "session_unarchived" | "message_enqueued" | "run_enqueued" | "run_leased" | "lease_expired" | "lease_recovered" | "run_suspended" | "run_retry_scheduled" | "run_completed" | "run_aborted" | "message_acknowledged" | "message_dead_lettered" | "tool_call_created" | "tool_call_started" | "tool_call_failed" | "tool_call_completed" | "tool_call_indeterminate";
88
91
  type RuntimeEvent = {
89
92
  id: RuntimeEventId;
90
93
  sequence: number;
@@ -325,9 +328,7 @@ interface PhaseContext {
325
328
  * Loaded Phase object
326
329
  */
327
330
  interface Phase {
328
- /** Unique identifier */
329
- id: string;
330
- /** Display name */
331
+ /** Unique phase identity and display name. */
331
332
  name: string;
332
333
  /** Description */
333
334
  description: string;
@@ -358,9 +359,9 @@ interface Phase {
358
359
  * Phase registry containing all loaded phases
359
360
  */
360
361
  interface PhaseRegistry {
361
- /** Map of phase id to Phase object */
362
+ /** Map of phase name to Phase object */
362
363
  phases: Map<string, Phase>;
363
- /** Entry phase id (null until Agent applies its default phase) */
364
+ /** Entry phase name (null until Agent applies its default phase) */
364
365
  entryPhaseId: string | null;
365
366
  }
366
367
 
@@ -372,7 +373,6 @@ interface PhaseRegistry {
372
373
  type PhaseRun = (context: PhaseContext, execution: PhaseExecution) => Promise<PhaseOutput | void>;
373
374
  /** Phase definition shape used by extensions */
374
375
  type PhaseDefinition = {
375
- id: string;
376
376
  name: string;
377
377
  description: string;
378
378
  run?: PhaseRun;
@@ -382,7 +382,7 @@ type PhaseDefinition = {
382
382
  input?: Record<string, string>;
383
383
  model?: string;
384
384
  };
385
- type PhaseRegistration = Partial<Omit<PhaseDefinition, 'run'>> & {
385
+ type PhaseRegistration = Omit<PhaseDefinition, 'run'> & {
386
386
  /** Optional execution override — takes over model invocation */
387
387
  run?: PhaseRun;
388
388
  };
@@ -480,7 +480,6 @@ interface ExtensionManifest {
480
480
  entry?: string;
481
481
  name?: string;
482
482
  phase?: {
483
- id?: string;
484
483
  name?: string;
485
484
  description?: string;
486
485
  tools?: string[];
@@ -603,7 +602,7 @@ interface EventBus {
603
602
  */
604
603
  interface BeforePhaseEvent {
605
604
  type: "before_phase";
606
- /** Phase ID about to execute */
605
+ /** Phase name about to execute */
607
606
  phaseId: string;
608
607
  /** Phase input */
609
608
  input: PhaseContext;
@@ -617,7 +616,7 @@ interface BeforePhaseEvent {
617
616
  */
618
617
  interface AfterPhaseEvent {
619
618
  type: "after_phase";
620
- /** Phase ID that completed */
619
+ /** Phase name that completed */
621
620
  phaseId: string;
622
621
  /** Phase output */
623
622
  output: PhaseOutput;
@@ -629,7 +628,7 @@ interface AfterPhaseEvent {
629
628
  */
630
629
  interface BeforePromptEvent {
631
630
  type: "before_prompt";
632
- /** Current phase ID */
631
+ /** Current phase name */
633
632
  phaseId: string;
634
633
  /** Phase input (modifiable) */
635
634
  input: PhaseContext;
@@ -961,10 +960,10 @@ interface ExtensionContext {
961
960
  }>;
962
961
  /** Get skill content by name */
963
962
  getSkillContent?(skillName: string): string;
964
- /** Get all available phase IDs */
963
+ /** Get all available phase names */
965
964
  getAvailablePhases?(): string[];
966
- /** Get phase content by ID */
967
- getPhaseContent?(phaseId: string): string;
965
+ /** Get phase content by name */
966
+ getPhaseContent?(phaseName: string): string;
968
967
  }
969
968
  /**
970
969
  * Utility functions available to extensions.
@@ -1033,10 +1032,10 @@ interface ExtensionAPI {
1033
1032
  setPayload(payload: unknown): void;
1034
1033
  /** Phase Out: set outcome message */
1035
1034
  setMessage(message: string): void;
1036
- /** Get current phase id */
1035
+ /** Get current phase name */
1037
1036
  getCurrentPhase(): string;
1038
1037
  /** Set next phase (lower priority than PHASE.md target) */
1039
- setNextPhase(phaseId: string): void;
1038
+ setNextPhase(phaseName: string): void;
1040
1039
  /** Get the next phase set by setNextPhase */
1041
1040
  getNextPhase(): string | undefined;
1042
1041
  /** Get the message set by setMessage */
@@ -1055,7 +1054,7 @@ type ExtensionFactory = (api: ExtensionAPI) => void | Promise<void>;
1055
1054
  * Scans for subdirectories containing PHASE.md files.
1056
1055
  * Returns PhaseRegistry with entryPhaseId:
1057
1056
  * - null by default (caller must explicitly set to start from a specific phase)
1058
- * - Set to a specific phase id to start from that phase
1057
+ * - Set to a specific phase name to start from that phase
1059
1058
  *
1060
1059
  * When entryPhaseId is null, Agent normalizes the registry to start from its default phase.
1061
1060
  */
@@ -1212,6 +1211,19 @@ declare const SkillSchema: Type.TObject<{
1212
1211
  }>;
1213
1212
  type Skill = Type.Static<typeof SkillSchema>;
1214
1213
 
1214
+ type SessionHeader = {
1215
+ type: "header";
1216
+ id: string;
1217
+ version: string;
1218
+ createdAt: string;
1219
+ updatedAt: string;
1220
+ systemPrompt: string;
1221
+ input: string;
1222
+ parentSessionId?: string;
1223
+ skills: Skill[];
1224
+ title?: string;
1225
+ currentLeafId?: string | null;
1226
+ };
1215
1227
  type SessionAgentContext<TTool = unknown> = {
1216
1228
  systemPrompt: string;
1217
1229
  messages: AgentMessage[];
@@ -1241,6 +1253,7 @@ type SessionListItem = {
1241
1253
  };
1242
1254
  type SessionManager = {
1243
1255
  getSessionId(): string;
1256
+ getHeader(): Promise<SessionHeader>;
1244
1257
  appendMessage(message: AgentMessage): Promise<string>;
1245
1258
  appendOutcome(outcome: Outcome): Promise<string>;
1246
1259
  appendModelTranscript(transcript: ModelTranscript, meta?: {
@@ -1252,10 +1265,13 @@ type SessionManager = {
1252
1265
  type SessionManagerProvider = {
1253
1266
  create(input: CreateSessionManagerInput): Promise<SessionManager>;
1254
1267
  open(sessionId: string): Promise<SessionManager | undefined>;
1268
+ list(): Promise<SessionListItem[]>;
1269
+ delete(sessionId: string): Promise<boolean>;
1255
1270
  };
1256
1271
 
1257
1272
  type CreateAgentInput = {
1258
1273
  sessionId: string;
1274
+ parentSessionId?: string;
1259
1275
  };
1260
1276
  type EnqueueAgentInput = {
1261
1277
  agentId: AgentId;
@@ -1344,6 +1360,9 @@ interface RuntimeStateStore {
1344
1360
  getAgent(agentId: AgentId): Promise<AgentRecord | undefined>;
1345
1361
  listAgents(): Promise<AgentRecord[]>;
1346
1362
  setAgentState(agentId: AgentId, state: AgentLifecycleState): Promise<AgentRecord>;
1363
+ setAgentLifecycleState(agentId: AgentId, state: SessionLifecycleState): Promise<AgentRecord>;
1364
+ getAgentBySessionId(sessionId: string): Promise<AgentRecord | undefined>;
1365
+ deleteAgentData(agentId: AgentId): Promise<void>;
1347
1366
  enqueueAgentInput(input: EnqueueAgentInput): Promise<EnqueuedAgentInput>;
1348
1367
  getMessage(messageId: RuntimeMessageId): Promise<RuntimeMessage | undefined>;
1349
1368
  getRun(runId: AgentRunId): Promise<AgentRunRecord | undefined>;
@@ -1399,6 +1418,17 @@ type RuntimeEventConsumer = {
1399
1418
  caughtUp: Promise<void>;
1400
1419
  stop(): void;
1401
1420
  };
1421
+ type RuntimeSessionSummary = {
1422
+ id: string;
1423
+ state: SessionLifecycleState;
1424
+ createdAt: string;
1425
+ updatedAt: string;
1426
+ archivedAt?: string;
1427
+ parentSessionId?: string;
1428
+ agentId: AgentId;
1429
+ activeRunCount: number;
1430
+ title?: string;
1431
+ };
1402
1432
  declare class AgentRuntime {
1403
1433
  private readonly stateStore;
1404
1434
  private readonly sessionProvider?;
@@ -1409,6 +1439,7 @@ declare class AgentRuntime {
1409
1439
  private readonly toolRuntime;
1410
1440
  private readonly bindings;
1411
1441
  private readonly agents;
1442
+ private readonly invalidatedAgents;
1412
1443
  private readonly runHandles;
1413
1444
  private readonly runWaiters;
1414
1445
  private readonly pendingRuns;
@@ -1436,6 +1467,13 @@ declare class AgentRuntime {
1436
1467
  getRun(runId: AgentRunId): Promise<AgentRunRecord | undefined>;
1437
1468
  listActiveRuns(): Promise<AgentRunRecord[]>;
1438
1469
  listRuns(input?: ListRunsInput): Promise<AgentRunRecord[]>;
1470
+ listSessions(input?: {
1471
+ state?: SessionLifecycleState;
1472
+ }): Promise<RuntimeSessionSummary[]>;
1473
+ archiveSession(sessionId: string): Promise<void>;
1474
+ unarchiveSession(sessionId: string): Promise<void>;
1475
+ deleteSession(sessionId: string): Promise<void>;
1476
+ private requireSessionAgent;
1439
1477
  getAgent(agentId: AgentId): Agent | undefined;
1440
1478
  getMessage(messageId: RuntimeMessageId): Promise<RuntimeMessage | undefined>;
1441
1479
  getToolCall(toolCallId: RuntimeToolCallId): Promise<RuntimeToolCall | undefined>;
@@ -1537,6 +1575,14 @@ declare class InMemorySessionStore implements SessionManagerProvider {
1537
1575
  private readonly sessions;
1538
1576
  create(input: CreateSessionManagerInput): Promise<SessionManager>;
1539
1577
  open(sessionId: string): Promise<SessionManager | undefined>;
1578
+ list(): Promise<{
1579
+ id: string;
1580
+ title: string | undefined;
1581
+ createdAt: string;
1582
+ updatedAt: string;
1583
+ messageCount: any;
1584
+ }[]>;
1585
+ delete(sessionId: string): Promise<boolean>;
1540
1586
  }
1541
1587
  declare class JsonlSessionStore implements SessionManagerProvider {
1542
1588
  private readonly sessionsDir;
@@ -1544,6 +1590,7 @@ declare class JsonlSessionStore implements SessionManagerProvider {
1544
1590
  create(input: CreateSessionManagerInput): Promise<SessionManager>;
1545
1591
  open(sessionId: string): Promise<SessionManager | undefined>;
1546
1592
  list(): Promise<SessionListItem[]>;
1593
+ delete(sessionId: string): Promise<boolean>;
1547
1594
  }
1548
1595
 
1549
1596
  type CoreToolContext = {
@@ -1563,8 +1610,11 @@ declare class InMemoryRuntimeStateStore implements RuntimeStateStore {
1563
1610
  private readonly eventCheckpoints;
1564
1611
  createAgent(input: CreateAgentInput): Promise<AgentRecord>;
1565
1612
  getAgent(agentId: AgentId): Promise<AgentRecord | undefined>;
1613
+ getAgentBySessionId(sessionId: string): Promise<AgentRecord | undefined>;
1566
1614
  listAgents(): Promise<AgentRecord[]>;
1567
1615
  setAgentState(agentId: AgentId, state: AgentRecord["state"]): Promise<AgentRecord>;
1616
+ setAgentLifecycleState(agentId: AgentId, state: AgentRecord["lifecycleState"]): Promise<AgentRecord>;
1617
+ deleteAgentData(agentId: AgentId): Promise<void>;
1568
1618
  enqueueAgentInput(input: EnqueueAgentInput): Promise<EnqueuedAgentInput>;
1569
1619
  private enqueueAgentInputState;
1570
1620
  getMessage(messageId: RuntimeMessageId): Promise<RuntimeMessage | undefined>;
@@ -1603,8 +1653,11 @@ declare class SqliteRuntimeStateStore implements RuntimeStateStore {
1603
1653
  close(): void;
1604
1654
  createAgent(input: CreateAgentInput): Promise<AgentRecord>;
1605
1655
  getAgent(agentId: AgentId): Promise<AgentRecord | undefined>;
1656
+ getAgentBySessionId(sessionId: string): Promise<AgentRecord | undefined>;
1606
1657
  listAgents(): Promise<AgentRecord[]>;
1607
1658
  setAgentState(agentId: AgentId, state: AgentRecord["state"]): Promise<AgentRecord>;
1659
+ setAgentLifecycleState(agentId: AgentId, state: AgentRecord["lifecycleState"]): Promise<AgentRecord>;
1660
+ deleteAgentData(agentId: AgentId): Promise<void>;
1608
1661
  enqueueAgentInput(input: EnqueueAgentInput): Promise<EnqueuedAgentInput>;
1609
1662
  private enqueueAgentInputState;
1610
1663
  getMessage(messageId: RuntimeMessageId): Promise<RuntimeMessage | undefined>;
@@ -1648,4 +1701,4 @@ declare class SqliteRuntimeStateStore implements RuntimeStateStore {
1648
1701
  private recordEventInternal;
1649
1702
  }
1650
1703
 
1651
- export { type AfterToolCall, Agent, type AgentContext, type AgentId, type AgentInputRequest, type AgentOptions, AgentRun, type AgentRunExecutionState, type AgentRunId, type AgentRunListener, type AgentRunMetadata, type AgentRunRecord, type AgentRunState, AgentRuntime, type AgentRuntimeOptions, type BeforeToolCall, type CoreToolContext, type ExecutionTurn, type ExtensionAPI, type ExtensionFactory, type HookEvent, type HookEventType, type HookHandler, InMemoryRuntimeStateStore, InMemorySessionStore, JsonlSessionStore, type LoadExtensionsResult, type LoadedExtension, type LoopMetrics, type ModelTranscript, type Phase, type PhaseContext, type PhaseExecution, type PhaseInvocation, type PhaseOutput, type PhaseRegistration, type PhaseRegistry, type PhaseState, type RuntimeEvent, type RuntimeEventConsumer, type RuntimeEventCursor, type RuntimeEventDisposition, type RuntimeEventKind, type RuntimeEventListener, type RuntimeMessage, type RuntimeMessageId, type RuntimeToolCall, type RuntimeToolCallId, type SessionManagerProvider, SqliteRuntimeStateStore, type Tool, type ToolContext, type ToolDefinition, type ToolExecutionMode, type ToolExecutionResult, type ToolRuntimePolicy, type Unsubscribe, createCoreTools, createMessage };
1704
+ export { type AfterToolCall, Agent, type AgentContext, type AgentId, type AgentInputRequest, type AgentOptions, AgentRun, type AgentRunExecutionState, type AgentRunId, type AgentRunListener, type AgentRunMetadata, type AgentRunRecord, type AgentRunState, AgentRuntime, type AgentRuntimeOptions, type BeforeToolCall, type CoreToolContext, type ExecutionTurn, type ExtensionAPI, type ExtensionFactory, type HookEvent, type HookEventType, type HookHandler, InMemoryRuntimeStateStore, InMemorySessionStore, JsonlSessionStore, type LoadExtensionsResult, type LoadedExtension, type LoopMetrics, type ModelTranscript, type Phase, type PhaseContext, type PhaseExecution, type PhaseInvocation, type PhaseOutput, type PhaseRegistration, type PhaseRegistry, type PhaseState, type RuntimeEvent, type RuntimeEventConsumer, type RuntimeEventCursor, type RuntimeEventDisposition, type RuntimeEventKind, type RuntimeEventListener, type RuntimeMessage, type RuntimeMessageId, type RuntimeSessionSummary, type RuntimeToolCall, type RuntimeToolCallId, type SessionLifecycleState, type SessionManagerProvider, SqliteRuntimeStateStore, type Tool, type ToolContext, type ToolDefinition, type ToolExecutionMode, type ToolExecutionResult, type ToolRuntimePolicy, type Unsubscribe, createCoreTools, createMessage };
package/dist/index.js CHANGED
@@ -387,51 +387,29 @@ import { dirname as dirname2, join, resolve } from "path";
387
387
  // src/harness/loader.ts
388
388
  import { readFile } from "fs/promises";
389
389
  import { basename, dirname, extname } from "path";
390
- function parseFrontmatter(raw) {
391
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
392
- if (!match) return { frontmatter: {}, body: raw };
393
- const lines = match[1].split("\n");
394
- const frontmatter = {};
395
- for (let i = 0; i < lines.length; i++) {
396
- const line = lines[i];
397
- const idx = line.indexOf(":");
398
- if (idx === -1) continue;
399
- const key = line.slice(0, idx).trim();
400
- const value = line.slice(idx + 1).trim();
401
- if (!key) continue;
402
- if (value === "" && i + 1 < lines.length) {
403
- const nested = {};
404
- while (i + 1 < lines.length) {
405
- const next = lines[i + 1];
406
- if (!next.startsWith(" ") || next.trim() === "") break;
407
- const nIdx = next.indexOf(":");
408
- if (nIdx === -1) break;
409
- const nKey = next.slice(0, nIdx).trim();
410
- const nValue = next.slice(nIdx + 1).trim();
411
- nested[nKey] = nValue;
412
- i++;
413
- }
414
- if (Object.keys(nested).length) {
415
- frontmatter[key] = nested;
416
- }
417
- continue;
418
- }
419
- frontmatter[key] = parseValue(value);
420
- }
421
- return { frontmatter, body: raw.slice(match[0].length) };
422
- }
423
- function parseValue(value) {
424
- const arrayMatch = value.match(/^\[(.*)\]$/);
425
- if (arrayMatch) {
426
- return arrayMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
390
+ import { parse as parseYaml } from "yaml";
391
+ var FrontmatterParseError = class extends Error {
392
+ constructor(message) {
393
+ super(message);
394
+ this.name = "FrontmatterParseError";
427
395
  }
428
- if (value.toLowerCase() === "true") return true;
429
- if (value.toLowerCase() === "false") return false;
430
- if (/^\d+$/.test(value)) return Number(value);
431
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
432
- return value.slice(1, -1);
396
+ };
397
+ function parseFrontmatter(raw) {
398
+ const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
399
+ if (!normalized.startsWith("---")) return { frontmatter: {}, body: normalized };
400
+ const endIndex = normalized.indexOf("\n---", 3);
401
+ if (endIndex === -1) return { frontmatter: {}, body: normalized };
402
+ const yamlString = normalized.slice(4, endIndex);
403
+ try {
404
+ const frontmatter = parseYaml(yamlString) ?? {};
405
+ return {
406
+ frontmatter,
407
+ body: normalized.slice(endIndex + 4).trim()
408
+ };
409
+ } catch (error) {
410
+ const message = error instanceof Error ? error.message : String(error);
411
+ throw new FrontmatterParseError(message);
433
412
  }
434
- return value;
435
413
  }
436
414
  async function loadMarkdown(filePath) {
437
415
  const raw = await readFile(filePath, "utf8");
@@ -543,6 +521,82 @@ function buildPhaseDirectiveMessage(phase, output) {
543
521
  return parts.join("\n");
544
522
  }
545
523
 
524
+ // src/harness/resource-validation.ts
525
+ var MAX_RESOURCE_ID_LENGTH = 64;
526
+ var MAX_DESCRIPTION_LENGTH = 1024;
527
+ var ResourceMetadataError = class extends Error {
528
+ constructor(code, message) {
529
+ super(message);
530
+ this.code = code;
531
+ this.name = "ResourceMetadataError";
532
+ }
533
+ code;
534
+ };
535
+ function validateResourceId(id, label = "id") {
536
+ const errors = [];
537
+ if (id.length > MAX_RESOURCE_ID_LENGTH) {
538
+ errors.push(`${label} exceeds ${MAX_RESOURCE_ID_LENGTH} characters (${id.length})`);
539
+ }
540
+ if (!/^[a-z0-9-]+$/.test(id)) {
541
+ errors.push(`${label} contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
542
+ }
543
+ if (id.startsWith("-") || id.endsWith("-")) {
544
+ errors.push(`${label} must not start or end with a hyphen`);
545
+ }
546
+ if (id.includes("--")) {
547
+ errors.push(`${label} must not contain consecutive hyphens`);
548
+ }
549
+ return errors;
550
+ }
551
+ function validateResourceName(name, id) {
552
+ const errors = [];
553
+ if (name !== id) {
554
+ errors.push(`name "${name}" does not match parent directory "${id}"`);
555
+ errors.push(...validateResourceId(name, "name"));
556
+ }
557
+ return errors;
558
+ }
559
+ function validateDescription(value) {
560
+ if (typeof value !== "string" || value.trim() === "") {
561
+ return {
562
+ warnings: ["description is required"],
563
+ missing: true
564
+ };
565
+ }
566
+ if (value.length > MAX_DESCRIPTION_LENGTH) {
567
+ return {
568
+ description: value,
569
+ warnings: [`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${value.length})`],
570
+ missing: false
571
+ };
572
+ }
573
+ return { description: value, warnings: [], missing: false };
574
+ }
575
+ function validateSkillReferences(value) {
576
+ if (value === void 0) return [];
577
+ if (!Array.isArray(value)) return ["skills must be an array"];
578
+ const errors = [];
579
+ for (const [index, skillId] of value.entries()) {
580
+ if (typeof skillId !== "string") {
581
+ errors.push(`skills[${index}] must be a string`);
582
+ continue;
583
+ }
584
+ errors.push(...validateResourceId(skillId, `skills[${index}]`));
585
+ }
586
+ return errors;
587
+ }
588
+ function validatePhaseTarget(value) {
589
+ if (value === void 0) return [];
590
+ if (typeof value !== "string") return ["target must be a string"];
591
+ if (value === "stop") return [];
592
+ return validateResourceId(value, "target");
593
+ }
594
+ function warnResourceDiagnostics(kind, filePath, diagnostics) {
595
+ for (const diagnostic of diagnostics) {
596
+ console.warn(`Invalid ${kind} metadata at "${filePath}": ${diagnostic}`);
597
+ }
598
+ }
599
+
546
600
  // src/harness/phases/loader.ts
547
601
  var PHASE_MARKER = "PHASE.md";
548
602
  function resolvePhasePath(input) {
@@ -551,28 +605,44 @@ function resolvePhasePath(input) {
551
605
  }
552
606
  async function loadPhase(targetPath) {
553
607
  const resolved = resolvePhasePath(targetPath);
554
- const { frontmatter, body } = await loadMarkdown(resolved);
555
- const id = inferResourceName(resolved, PHASE_MARKER);
556
- if (!frontmatter.name) {
557
- throw new Error(`Phase "${id}" at "${resolved}" is missing required field "name" in PHASE.md frontmatter.`);
558
- }
559
- if (!frontmatter.description) {
560
- throw new Error(`Phase "${id}" at "${resolved}" is missing required field "description" in PHASE.md frontmatter.`);
608
+ let loaded;
609
+ try {
610
+ loaded = await loadMarkdown(resolved);
611
+ } catch (error) {
612
+ if (!(error instanceof FrontmatterParseError)) throw error;
613
+ warnResourceDiagnostics("phase", resolved, [`frontmatter could not be parsed: ${error.message}`]);
614
+ throw new ResourceMetadataError("parse_failed", error.message);
615
+ }
616
+ const directoryName = inferResourceName(resolved, PHASE_MARKER);
617
+ const { frontmatter, body } = loaded;
618
+ const metadata = frontmatter;
619
+ const frontmatterName = typeof metadata.name === "string" ? metadata.name : void 0;
620
+ const name = frontmatterName || directoryName;
621
+ const description = validateDescription(metadata.description);
622
+ const diagnostics = validateResourceId(directoryName);
623
+ if (frontmatterName && frontmatterName !== directoryName) {
624
+ diagnostics.push(...validateResourceName(frontmatterName, directoryName));
625
+ }
626
+ diagnostics.push(...description.warnings);
627
+ diagnostics.push(...validateSkillReferences(metadata.skills));
628
+ diagnostics.push(...validatePhaseTarget(metadata.target));
629
+ warnResourceDiagnostics("phase", resolved, diagnostics);
630
+ if (description.missing) {
631
+ throw new ResourceMetadataError("invalid_metadata", "description is required");
561
632
  }
562
633
  const baseDir = dirname2(resolved);
563
634
  const phase = {
564
- id,
565
- name: frontmatter.name,
566
- description: frontmatter.description,
567
- tools: frontmatter.tools,
568
- skills: frontmatter.skills,
569
- target: frontmatter.target,
570
- input: frontmatter.input,
571
- isolated: frontmatter.isolated,
635
+ name,
636
+ description: description.description,
637
+ tools: metadata.tools,
638
+ skills: metadata.skills,
639
+ target: metadata.target,
640
+ input: metadata.input,
641
+ isolated: metadata.isolated,
572
642
  filePath: resolved,
573
643
  baseDir,
574
644
  content: body,
575
- model: parseModelRef(frontmatter.model)
645
+ model: parseModelRef(metadata.model)
576
646
  };
577
647
  const codePath = await discoverPhaseCode(baseDir);
578
648
  if (codePath) {
@@ -598,13 +668,21 @@ async function loadPhases(targetPath) {
598
668
  const phases = /* @__PURE__ */ new Map();
599
669
  const phasesDir = resolve(targetPath);
600
670
  if (existsSync(phasesDir) && statSync(phasesDir).isFile()) {
601
- const phase = await loadPhase(phasesDir);
602
- phases.set(phase.id, phase);
671
+ try {
672
+ const phase = await loadPhase(phasesDir);
673
+ phases.set(phase.name, phase);
674
+ } catch (error) {
675
+ if (!(error instanceof ResourceMetadataError)) throw error;
676
+ }
603
677
  return { phases, entryPhaseId: null };
604
678
  }
605
679
  if (existsSync(join(phasesDir, PHASE_MARKER))) {
606
- const phase = await loadPhase(phasesDir);
607
- phases.set(phase.id, phase);
680
+ try {
681
+ const phase = await loadPhase(phasesDir);
682
+ phases.set(phase.name, phase);
683
+ } catch (error) {
684
+ if (!(error instanceof ResourceMetadataError)) throw error;
685
+ }
608
686
  return { phases, entryPhaseId: null };
609
687
  }
610
688
  const entries = await readdir(phasesDir, { withFileTypes: true });
@@ -614,8 +692,9 @@ async function loadPhases(targetPath) {
614
692
  if (!existsSync(phaseFile)) continue;
615
693
  try {
616
694
  const phase = await loadPhase(phaseFile);
617
- phases.set(phase.id, phase);
695
+ phases.set(phase.name, phase);
618
696
  } catch (error) {
697
+ if (error instanceof ResourceMetadataError) continue;
619
698
  console.warn(`Failed to load phase "${entry.name}":`, error);
620
699
  }
621
700
  }
@@ -653,8 +732,7 @@ async function loadPhaseCode(codePath) {
653
732
  var DEFAULT_PHASE_ID = "default";
654
733
  function createDefaultPhase() {
655
734
  return {
656
- id: DEFAULT_PHASE_ID,
657
- name: "Execution Phase",
735
+ name: DEFAULT_PHASE_ID,
658
736
  description: "Executes concrete task operations and produces artifacts.",
659
737
  filePath: "",
660
738
  baseDir: "",
@@ -678,7 +756,7 @@ function normalizeRelativePath(path) {
678
756
  import Type from "typebox";
679
757
  var PhaseRouteTool = "route";
680
758
  function buildPhaseEntry(p) {
681
- const entry = { id: p.id, description: p.description };
759
+ const entry = { name: p.name, description: p.description };
682
760
  if (p.tools && p.tools.length > 0) {
683
761
  entry.available_tools = p.tools.join(", ");
684
762
  }
@@ -693,7 +771,7 @@ function buildPhaseEntry(p) {
693
771
  function buildRouteDescription(availablePhases) {
694
772
  const phasesBlock = buildStructuredSection("phase", [
695
773
  ...availablePhases.map(buildPhaseEntry),
696
- { id: "stop", description: "Terminate the workflow and return final result to the user" }
774
+ { name: "stop", description: "Terminate the workflow and return final result to the user" }
697
775
  ]);
698
776
  return [
699
777
  "Route execution to one or more phases concurrently, or stop execution.",
@@ -714,7 +792,7 @@ function buildRouteDescription(availablePhases) {
714
792
  function createRouteTool(availablePhases) {
715
793
  const DecisionTarget = Type.Object({
716
794
  phase: Type.Union([
717
- ...availablePhases.map((p) => Type.Literal(p.id)),
795
+ ...availablePhases.map((p) => Type.Literal(p.name)),
718
796
  Type.Literal("stop")
719
797
  ]),
720
798
  reason: Type.Optional(Type.String({ description: "Brief reason for this decision" })),
@@ -1222,8 +1300,7 @@ function createCoreTools(input = {}) {
1222
1300
 
1223
1301
  // src/harness/context/system-prompt.ts
1224
1302
  function buildSystemPrompt(options) {
1225
- const { systemPrompt, promptGuidelines, appendSystemPrompt, tools, skills, cwd } = options;
1226
- const date = createTimestamp();
1303
+ const { systemPrompt, promptGuidelines, appendSystemPrompt, tools, skills } = options;
1227
1304
  const guidelinesList = [];
1228
1305
  const guidelinesSet = /* @__PURE__ */ new Set();
1229
1306
  const addGuideline = (guideline) => {
@@ -1239,7 +1316,8 @@ function buildSystemPrompt(options) {
1239
1316
  addGuideline(g);
1240
1317
  }
1241
1318
  }
1242
- const skillsBlock = skills && skills.length > 0 ? buildSkillsDescription(skills) : "";
1319
+ const visibleSkills = (skills ?? []).filter((s) => !s.disableModelInvocation);
1320
+ const skillsBlock = visibleSkills.length > 0 ? buildSkillsDescription(visibleSkills) : "";
1243
1321
  for (const g of promptGuidelines ?? []) {
1244
1322
  addGuideline(g);
1245
1323
  }
@@ -1260,14 +1338,6 @@ ${toolsList}
1260
1338
  ${skillsSection}
1261
1339
  Guidelines:
1262
1340
  ${guidelines}`;
1263
- if (date || cwd) {
1264
- const contextParts = [];
1265
- if (date) contextParts.push(`Current date: ${date}`);
1266
- if (cwd) contextParts.push(`Working directory: ${cwd}`);
1267
- prompt += `
1268
-
1269
- ${contextParts.join("\n")}`;
1270
- }
1271
1341
  if (appendSystemPrompt) {
1272
1342
  prompt += `
1273
1343
 
@@ -1444,12 +1514,12 @@ function injectPhaseContent(phase, output, messages, mirror) {
1444
1514
  try {
1445
1515
  const phaseContent = phase.filePath ? readPhaseContent(phase) : phase.content ?? phase.description ?? "";
1446
1516
  const content = buildPhaseDirectiveMessage(
1447
- { name: phase.id, content: phaseContent },
1517
+ { name: phase.name, content: phaseContent },
1448
1518
  output
1449
1519
  );
1450
1520
  const message = createMessage("user", content, {
1451
1521
  kind: "phase_prompt",
1452
- phase: phase.id
1522
+ phase: phase.name
1453
1523
  });
1454
1524
  messages.push(message);
1455
1525
  if (mirror && mirror !== messages) mirror.push(message);
@@ -1594,7 +1664,7 @@ async function executePhaseWithModel(ctx) {
1594
1664
  for (const toolCall of collected.toolCalls) {
1595
1665
  if (toolCall.name !== PhaseRouteTool) continue;
1596
1666
  const messageId = ctx.messageManager.start("tool", createRouteToolResultContent(toolCall), {
1597
- phase: ctx.phase.id
1667
+ phase: ctx.phase.name
1598
1668
  });
1599
1669
  await ctx.messageManager.end(messageId);
1600
1670
  }
@@ -1607,7 +1677,7 @@ async function executePhaseWithModel(ctx) {
1607
1677
  for (const toolCall of executableToolCalls) {
1608
1678
  const result = await ctx.execution.executeTool(roundContext, toolCall);
1609
1679
  const messageId = ctx.messageManager.start("tool", createToolResultContent(result), {
1610
- phase: ctx.phase.id
1680
+ phase: ctx.phase.name
1611
1681
  });
1612
1682
  await ctx.messageManager.end(messageId);
1613
1683
  }
@@ -1646,7 +1716,7 @@ async function runPhaseLoop(config, state, registry) {
1646
1716
  while (currentPhaseId) {
1647
1717
  const availablePhases = [];
1648
1718
  for (const [, phase2] of registry.phases) {
1649
- availablePhases.push({ id: phase2.id, name: phase2.name, description: phase2.description, tools: phase2.tools, skills: phase2.skills, input: phase2.input, isolated: phase2.isolated });
1719
+ availablePhases.push({ name: phase2.name, description: phase2.description, tools: phase2.tools, skills: phase2.skills, input: phase2.input, isolated: phase2.isolated });
1650
1720
  }
1651
1721
  const abortResult = LoopGuard.checkAbort(config.signal);
1652
1722
  if (abortResult.stopReason !== "none") {
@@ -1759,7 +1829,7 @@ async function runPhaseLoop(config, state, registry) {
1759
1829
  } else if (latestUserInputMessage) {
1760
1830
  const phaseInputMessage = createMessage("user", latestUserInputMessage.content, {
1761
1831
  kind: "phase_input",
1762
- phase: phase.id
1832
+ phase: phase.name
1763
1833
  });
1764
1834
  config.context.messages.push(phaseInputMessage);
1765
1835
  phaseContext.messages.push(phaseInputMessage);
@@ -1931,7 +2001,7 @@ async function runPhaseLoop(config, state, registry) {
1931
2001
  ts: createTimestamp()
1932
2002
  });
1933
2003
  previousPayload = output.payload;
1934
- previousResults = output.payload !== void 0 ? [{ name: phase.id, output: output.payload }] : [];
2004
+ previousResults = output.payload !== void 0 ? [{ name: phase.name, output: output.payload }] : [];
1935
2005
  currentPhaseId = targetPhaseId;
1936
2006
  }
1937
2007
  throw new Error("Phase machine exited without a stop or abort transition.");
@@ -2084,7 +2154,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2084
2154
  },
2085
2155
  async invokeModel(phaseContext) {
2086
2156
  if (config.beforePrompt) {
2087
- phaseContext = await config.beforePrompt(phase.id, phaseContext);
2157
+ phaseContext = await config.beforePrompt(phase.name, phaseContext);
2088
2158
  }
2089
2159
  const request = buildModelRequest({
2090
2160
  systemPrompt: phaseContext.systemPrompt,
@@ -2111,7 +2181,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2111
2181
  config,
2112
2182
  message: messageManager,
2113
2183
  request,
2114
- phaseId: phase.id
2184
+ phaseId: phase.name
2115
2185
  }),
2116
2186
  {
2117
2187
  signal: config.signal,
@@ -2133,7 +2203,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2133
2203
  }
2134
2204
  )
2135
2205
  );
2136
- await config.onModelTranscript?.(result.transcript, { phase: phase.id, model: phase.model ?? config.model });
2206
+ await config.onModelTranscript?.(result.transcript, { phase: phase.name, model: phase.model ?? config.model });
2137
2207
  return result;
2138
2208
  },
2139
2209
  async executeTool(_context, toolCall) {
@@ -2166,7 +2236,7 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2166
2236
  sourcePhaseId
2167
2237
  },
2168
2238
  state: {
2169
- current: phase.id,
2239
+ current: phase.name,
2170
2240
  available: Array.from(registry.phases.keys()),
2171
2241
  iterations: 0,
2172
2242
  payload
@@ -2185,7 +2255,7 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2185
2255
  if (phaseMsgId) removePhaseMessage(messages, phaseMsgId);
2186
2256
  const decision = output.toolCalls ? extractRouteCall(output.toolCalls) : void 0;
2187
2257
  const resultPayload = decision?.decision[0]?.payload !== void 0 ? normalizePayload(decision.decision[0].payload) : output.payload;
2188
- return { instanceId, phaseId: phase.id, payload: resultPayload, content: output.message };
2258
+ return { instanceId, phaseId: phase.name, payload: resultPayload, content: output.message };
2189
2259
  }
2190
2260
  async function waitForBackgroundTasks(backgroundTasks) {
2191
2261
  const entries = Array.from(backgroundTasks.entries());
@@ -2273,23 +2343,55 @@ function readSkillContent(skill) {
2273
2343
  }
2274
2344
  async function loadSkill(path) {
2275
2345
  const resolved = resolveSkillPath(path);
2276
- const { frontmatter, body } = await loadMarkdown(resolved);
2346
+ let loaded;
2347
+ try {
2348
+ loaded = await loadMarkdown(resolved);
2349
+ } catch (error) {
2350
+ if (!(error instanceof FrontmatterParseError)) throw error;
2351
+ warnResourceDiagnostics("skill", resolved, [`frontmatter could not be parsed: ${error.message}`]);
2352
+ throw new ResourceMetadataError("parse_failed", error.message);
2353
+ }
2354
+ const { frontmatter, body } = loaded;
2355
+ const metadata = frontmatter;
2356
+ const id = inferResourceName(resolved, SKILL_MARKER);
2357
+ const frontmatterName = typeof metadata.name === "string" ? metadata.name : void 0;
2358
+ const name = frontmatterName || id;
2359
+ const description = validateDescription(metadata.description);
2360
+ const diagnostics = validateResourceId(id);
2361
+ if (frontmatterName && frontmatterName !== id) {
2362
+ diagnostics.push(...validateResourceName(frontmatterName, id));
2363
+ }
2364
+ diagnostics.push(...description.warnings);
2365
+ warnResourceDiagnostics("skill", resolved, diagnostics);
2366
+ if (description.missing) {
2367
+ throw new ResourceMetadataError("invalid_metadata", "description is required");
2368
+ }
2277
2369
  return {
2278
- name: frontmatter.name ?? inferResourceName(resolved, SKILL_MARKER),
2279
- description: frontmatter.description ?? "",
2370
+ name,
2371
+ description: description.description,
2280
2372
  filePath: resolved,
2281
2373
  baseDir: dirname4(resolved),
2282
2374
  content: body,
2283
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
2375
+ disableModelInvocation: metadata["disable-model-invocation"] === true
2284
2376
  };
2285
2377
  }
2286
2378
  async function loadSkills(targetPath) {
2287
2379
  const skillsDir = resolve4(targetPath);
2288
2380
  if (existsSync2(skillsDir) && statSync2(skillsDir).isFile()) {
2289
- return [await loadSkill(skillsDir)];
2381
+ try {
2382
+ return [await loadSkill(skillsDir)];
2383
+ } catch (error) {
2384
+ if (error instanceof ResourceMetadataError) return [];
2385
+ throw error;
2386
+ }
2290
2387
  }
2291
2388
  if (existsSync2(join2(skillsDir, SKILL_MARKER))) {
2292
- return [await loadSkill(skillsDir)];
2389
+ try {
2390
+ return [await loadSkill(skillsDir)];
2391
+ } catch (error) {
2392
+ if (error instanceof ResourceMetadataError) return [];
2393
+ throw error;
2394
+ }
2293
2395
  }
2294
2396
  const entries = await readdir2(skillsDir, { withFileTypes: true });
2295
2397
  const skills = [];
@@ -2301,6 +2403,7 @@ async function loadSkills(targetPath) {
2301
2403
  const skill = await loadSkill(skillFile);
2302
2404
  skills.push(skill);
2303
2405
  } catch (error) {
2406
+ if (error instanceof ResourceMetadataError) continue;
2304
2407
  console.warn(`Failed to load skill "${entry.name}":`, error);
2305
2408
  }
2306
2409
  }
@@ -2562,12 +2665,16 @@ function applyProviderRegistration(config) {
2562
2665
  protocol: config.protocol,
2563
2666
  provider: config.id,
2564
2667
  baseUrl: config.baseUrl,
2668
+ apiKey: config.apiKey,
2565
2669
  reasoning: modelConfig.reasoning,
2566
2670
  input: modelConfig.input,
2567
2671
  cost: modelConfig.cost,
2568
2672
  contextWindow: modelConfig.contextWindow,
2569
2673
  maxTokens: modelConfig.maxTokens,
2570
- ...config.headers ? { headers: config.headers } : {}
2674
+ ...config.headers ? { headers: config.headers } : {},
2675
+ ...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
2676
+ ...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
2677
+ ...config.retryDelayMs !== void 0 ? { retryDelayMs: config.retryDelayMs } : {}
2571
2678
  });
2572
2679
  }
2573
2680
  }
@@ -2794,8 +2901,8 @@ var ExtensionRunner = class {
2794
2901
  // ---------------------------------------------------------------------------
2795
2902
  // Phase management
2796
2903
  // ---------------------------------------------------------------------------
2797
- getPhase(id) {
2798
- const reg = this.getRegisteredPhase(id);
2904
+ getPhase(name) {
2905
+ const reg = this.getRegisteredPhase(name);
2799
2906
  if (!reg) return void 0;
2800
2907
  return this.adaptToPhase(reg);
2801
2908
  }
@@ -2807,8 +2914,8 @@ var ExtensionRunner = class {
2807
2914
  createPhaseRegistry(input = {}) {
2808
2915
  const registered = this.collectRegisteredPhases();
2809
2916
  const phases = /* @__PURE__ */ new Map();
2810
- for (const [id, reg] of registered) {
2811
- phases.set(id, this.adaptToPhase(reg));
2917
+ for (const [name, reg] of registered) {
2918
+ phases.set(name, this.adaptToPhase(reg));
2812
2919
  }
2813
2920
  const entryPhaseId = input.entryPhaseId ?? null;
2814
2921
  return { phases, entryPhaseId };
@@ -2817,9 +2924,8 @@ var ExtensionRunner = class {
2817
2924
  adaptToPhase(reg) {
2818
2925
  const def = reg.definition;
2819
2926
  return {
2820
- id: def.id,
2821
- name: def.name ?? def.id,
2822
- description: def.description ?? "",
2927
+ name: def.name,
2928
+ description: def.description,
2823
2929
  tools: def.tools,
2824
2930
  skills: def.skills,
2825
2931
  target: def.target,
@@ -3086,22 +3192,27 @@ var ExtensionRunner = class {
3086
3192
  });
3087
3193
  }
3088
3194
  registerPhase(extension, registration) {
3089
- if (!registration.id) {
3090
- throw new Error(`Phase registration requires an "id" field.`);
3091
- }
3092
- if (!registration.name) {
3093
- throw new Error(`Phase registration "${registration.id}" requires a "name" field.`);
3195
+ if (typeof registration.name !== "string" || registration.name.length === 0) {
3196
+ throw new Error(`Phase registration requires a "name" field.`);
3197
+ }
3198
+ const name = registration.name;
3199
+ const description = validateDescription(registration.description);
3200
+ const errors = validateResourceId(name, "name");
3201
+ if (description.missing) errors.push(...description.warnings);
3202
+ errors.push(...validateSkillReferences(registration.skills));
3203
+ errors.push(...validatePhaseTarget(registration.target));
3204
+ if (errors.length > 0) {
3205
+ throw new Error(`Invalid phase registration "${name}": ${errors.join("; ")}`);
3094
3206
  }
3095
- if (!registration.description) {
3096
- throw new Error(`Phase registration "${registration.id}" requires a "description" field.`);
3207
+ if (description.warnings.length > 0) {
3208
+ warnResourceDiagnostics("phase", `extension ${extension.path}`, description.warnings);
3097
3209
  }
3098
- if (this.phases.has(registration.id)) {
3099
- throw new Error(`Duplicate phase id: ${registration.id}`);
3210
+ if (this.phases.has(name)) {
3211
+ throw new Error(`Duplicate phase name: ${name}`);
3100
3212
  }
3101
3213
  const definition = {
3102
- id: registration.id,
3103
- name: registration.name,
3104
- description: registration.description,
3214
+ name,
3215
+ description: description.description,
3105
3216
  run: registration.run,
3106
3217
  ...registration.model ? { model: registration.model } : {}
3107
3218
  };
@@ -3109,8 +3220,8 @@ var ExtensionRunner = class {
3109
3220
  definition,
3110
3221
  source: { extensionPath: extension.path }
3111
3222
  };
3112
- this.phases.set(registration.id, registered);
3113
- extension.phases.set(registration.id, registered);
3223
+ this.phases.set(name, registered);
3224
+ extension.phases.set(name, registered);
3114
3225
  this._phaseCache = null;
3115
3226
  }
3116
3227
  registerProvider(config) {
@@ -3137,8 +3248,8 @@ var ExtensionRunner = class {
3137
3248
  }
3138
3249
  this.pendingProviders.length = 0;
3139
3250
  }
3140
- getRegisteredPhase(id) {
3141
- return this.collectRegisteredPhases().get(id);
3251
+ getRegisteredPhase(name) {
3252
+ return this.collectRegisteredPhases().get(name);
3142
3253
  }
3143
3254
  collectRegisteredPhases() {
3144
3255
  if (this._phaseCache) return this._phaseCache;
@@ -4472,6 +4583,22 @@ var InMemorySessionStore = class {
4472
4583
  async open(sessionId) {
4473
4584
  return this.sessions.get(sessionId);
4474
4585
  }
4586
+ async list() {
4587
+ return Promise.all([...this.sessions.values()].map(async (session) => {
4588
+ const header = await session.getHeader();
4589
+ const entries = "listEntries" in session && typeof session.listEntries === "function" ? await session.listEntries() : [];
4590
+ return {
4591
+ id: header.id,
4592
+ title: header.title,
4593
+ createdAt: header.createdAt,
4594
+ updatedAt: header.updatedAt,
4595
+ messageCount: entries.filter((entry) => entry.type === "message").length
4596
+ };
4597
+ }));
4598
+ }
4599
+ async delete(sessionId) {
4600
+ return this.sessions.delete(sessionId);
4601
+ }
4475
4602
  };
4476
4603
  var JsonlSessionStore = class {
4477
4604
  constructor(sessionsDir) {
@@ -4487,6 +4614,9 @@ var JsonlSessionStore = class {
4487
4614
  list() {
4488
4615
  return LocalJsonlSessionManager.list(this.sessionsDir);
4489
4616
  }
4617
+ delete(sessionId) {
4618
+ return LocalJsonlSessionManager.delete(this.sessionsDir, sessionId);
4619
+ }
4490
4620
  };
4491
4621
 
4492
4622
  // src/runtime/agent-runtime.ts
@@ -4762,6 +4892,7 @@ var AgentRuntime = class _AgentRuntime {
4762
4892
  toolRuntime;
4763
4893
  bindings = /* @__PURE__ */ new Map();
4764
4894
  agents = /* @__PURE__ */ new Map();
4895
+ invalidatedAgents = /* @__PURE__ */ new Set();
4765
4896
  runHandles = /* @__PURE__ */ new Map();
4766
4897
  runWaiters = /* @__PURE__ */ new Map();
4767
4898
  pendingRuns = [];
@@ -4808,6 +4939,15 @@ var AgentRuntime = class _AgentRuntime {
4808
4939
  const runtime = new _AgentRuntime(options);
4809
4940
  activeRuntime = runtime;
4810
4941
  try {
4942
+ if (options.sessionProvider) {
4943
+ for (const record of await options.stateStore.listAgents()) {
4944
+ if (record.lifecycleState !== "deleting") continue;
4945
+ const activeRuns = await options.stateStore.listRuns({ agentId: record.id, states: ["queued", "running", "suspended"] });
4946
+ if (activeRuns.length > 0) continue;
4947
+ await options.stateStore.deleteAgentData(record.id);
4948
+ await options.sessionProvider.delete(record.sessionId);
4949
+ }
4950
+ }
4811
4951
  await runtime.recover();
4812
4952
  runtime.startLeaseRecovery();
4813
4953
  return runtime;
@@ -4826,8 +4966,10 @@ var AgentRuntime = class _AgentRuntime {
4826
4966
  input: "",
4827
4967
  skills: options.context.skills
4828
4968
  });
4969
+ const header = await manager.getHeader();
4829
4970
  const record = await this.stateStore.createAgent({
4830
- sessionId: manager.getSessionId()
4971
+ sessionId: manager.getSessionId(),
4972
+ parentSessionId: header.parentSessionId
4831
4973
  });
4832
4974
  this.pausedAgents.delete(record.id);
4833
4975
  const agent = await this.attachAgent(record, manager, options);
@@ -4839,6 +4981,7 @@ var AgentRuntime = class _AgentRuntime {
4839
4981
  assertAgentOptions(options);
4840
4982
  const record = await this.stateStore.getAgent(agentId);
4841
4983
  if (!record) throw new Error(`Agent not found: ${agentId}.`);
4984
+ if (record.lifecycleState !== "active") throw new Error(`Session ${record.sessionId} is ${record.lifecycleState}.`);
4842
4985
  if (record.state === "paused") this.pausedAgents.add(record.id);
4843
4986
  else this.pausedAgents.delete(record.id);
4844
4987
  const provider = this.sessionProvider;
@@ -4947,6 +5090,82 @@ var AgentRuntime = class _AgentRuntime {
4947
5090
  async listRuns(input) {
4948
5091
  return this.stateStore.listRuns(input);
4949
5092
  }
5093
+ async listSessions(input = {}) {
5094
+ const records = await this.stateStore.listAgents();
5095
+ const items = this.sessionProvider ? await this.sessionProvider.list() : [];
5096
+ const summaries = new Map(items.map((item) => [item.id, item]));
5097
+ const activeRuns = await this.stateStore.listActiveRuns();
5098
+ return records.filter((record) => !input.state || record.lifecycleState === input.state).map((record) => {
5099
+ const item = summaries.get(record.sessionId);
5100
+ return {
5101
+ id: record.sessionId,
5102
+ state: record.lifecycleState,
5103
+ createdAt: record.createdAt,
5104
+ updatedAt: item?.updatedAt ?? record.updatedAt,
5105
+ ...record.lifecycleState === "archived" ? { archivedAt: record.updatedAt } : {},
5106
+ ...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
5107
+ agentId: record.id,
5108
+ activeRunCount: activeRuns.filter((run) => run.agentId === record.id).length,
5109
+ ...item?.title ? { title: item.title } : {}
5110
+ };
5111
+ });
5112
+ }
5113
+ async archiveSession(sessionId) {
5114
+ const record = await this.requireSessionAgent(sessionId);
5115
+ if (record.lifecycleState === "archived") return;
5116
+ if (record.lifecycleState === "deleting") throw new Error(`Session ${sessionId} is being deleted.`);
5117
+ await this.stateStore.setAgentLifecycleState(record.id, "archived");
5118
+ this.publishEvents();
5119
+ }
5120
+ async unarchiveSession(sessionId) {
5121
+ const record = await this.requireSessionAgent(sessionId);
5122
+ if (record.lifecycleState === "active") return;
5123
+ if (record.lifecycleState === "deleting") throw new Error(`Session ${sessionId} is being deleted.`);
5124
+ await this.stateStore.setAgentLifecycleState(record.id, "active");
5125
+ await this.scheduleQueuedRuns(record.id);
5126
+ this.publishEvents();
5127
+ }
5128
+ async deleteSession(sessionId) {
5129
+ this.assertRunning();
5130
+ if (!this.sessionProvider) throw new Error("Agent Runtime requires a SessionManager provider to delete a Session.");
5131
+ if (!this.sessionProvider.delete) throw new Error("Session Manager provider does not support deletion.");
5132
+ const root = await this.requireSessionAgent(sessionId);
5133
+ const records = await this.stateStore.listAgents();
5134
+ const subtree = /* @__PURE__ */ new Set([root.id]);
5135
+ let changed = true;
5136
+ while (changed) {
5137
+ changed = false;
5138
+ for (const record of records) {
5139
+ if (record.parentSessionId && records.some((candidate) => subtree.has(candidate.id) && candidate.sessionId === record.parentSessionId) && !subtree.has(record.id)) {
5140
+ subtree.add(record.id);
5141
+ changed = true;
5142
+ }
5143
+ }
5144
+ }
5145
+ const activeRuns = await this.stateStore.listActiveRuns();
5146
+ if (activeRuns.some((run) => subtree.has(run.agentId))) {
5147
+ throw new Error(`Session ${sessionId} has active Runs and cannot be deleted.`);
5148
+ }
5149
+ const targets = records.filter((record) => subtree.has(record.id));
5150
+ for (const record of targets) await this.stateStore.setAgentLifecycleState(record.id, "deleting");
5151
+ for (const record of targets) {
5152
+ this.bindings.delete(record.id);
5153
+ this.agents.delete(record.id);
5154
+ this.invalidatedAgents.add(record.id);
5155
+ this.pausedAgents.delete(record.id);
5156
+ for (let index = this.pendingRuns.length - 1; index >= 0; index -= 1) {
5157
+ if (this.pendingRuns[index]?.agentId === record.id) this.pendingRuns.splice(index, 1);
5158
+ }
5159
+ await this.stateStore.deleteAgentData(record.id);
5160
+ await this.sessionProvider.delete(record.sessionId);
5161
+ }
5162
+ this.publishEvents();
5163
+ }
5164
+ async requireSessionAgent(sessionId) {
5165
+ const record = await this.stateStore.getAgentBySessionId(sessionId);
5166
+ if (!record) throw new Error(`Session not found: ${sessionId}.`);
5167
+ return record;
5168
+ }
4950
5169
  getAgent(agentId) {
4951
5170
  return this.agents.get(agentId);
4952
5171
  }
@@ -5079,7 +5298,11 @@ var AgentRuntime = class _AgentRuntime {
5079
5298
  }
5080
5299
  async submitAgentInput(agentId, input, persist) {
5081
5300
  this.assertRunning();
5301
+ if (this.invalidatedAgents.has(agentId)) throw new Error(`Session for Agent ${agentId} was deleted.`);
5082
5302
  if (!this.bindings.has(agentId)) throw new Error(`Agent ${agentId} has no live Agent Binding.`);
5303
+ const record = await this.stateStore.getAgent(agentId);
5304
+ if (!record) throw new Error(`Agent ${agentId} was deleted.`);
5305
+ if (record.lifecycleState !== "active") throw new Error(`Session ${record.sessionId} is ${record.lifecycleState}.`);
5083
5306
  const enqueued = await this.stateStore.enqueueAgentInput({ agentId, input });
5084
5307
  await persist();
5085
5308
  if (enqueued.resumed) this.notifyRun(enqueued.run);
@@ -5335,6 +5558,8 @@ var InMemoryRuntimeStateStore = class {
5335
5558
  id: createId("agt"),
5336
5559
  sessionId: input.sessionId,
5337
5560
  state: "active",
5561
+ lifecycleState: "active",
5562
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
5338
5563
  createdAt: timestamp,
5339
5564
  updatedAt: timestamp
5340
5565
  };
@@ -5346,6 +5571,10 @@ var InMemoryRuntimeStateStore = class {
5346
5571
  const agent = this.agents.get(agentId);
5347
5572
  return agent ? clone2(agent) : void 0;
5348
5573
  }
5574
+ async getAgentBySessionId(sessionId) {
5575
+ const agent = [...this.agents.values()].find((candidate) => candidate.sessionId === sessionId);
5576
+ return agent ? clone2(agent) : void 0;
5577
+ }
5349
5578
  async listAgents() {
5350
5579
  return clone2([...this.agents.values()]);
5351
5580
  }
@@ -5358,6 +5587,27 @@ var InMemoryRuntimeStateStore = class {
5358
5587
  this.recordEvent(state === "paused" ? "agent_paused" : "agent_resumed", { agentId });
5359
5588
  return clone2(agent);
5360
5589
  }
5590
+ async setAgentLifecycleState(agentId, state) {
5591
+ const agent = this.requireAgent(agentId);
5592
+ if (agent.lifecycleState === state) return clone2(agent);
5593
+ agent.lifecycleState = state;
5594
+ agent.updatedAt = createTimestamp();
5595
+ if (state === "archived") this.recordEvent("session_archived", { agentId });
5596
+ if (state === "active") this.recordEvent("session_unarchived", { agentId });
5597
+ return clone2(agent);
5598
+ }
5599
+ async deleteAgentData(agentId) {
5600
+ this.requireAgent(agentId);
5601
+ const runIds = new Set([...this.runs.values()].filter((run) => run.agentId === agentId).map((run) => run.id));
5602
+ for (const [id, call] of this.toolCalls) if (call.agentId === agentId || runIds.has(call.runId)) this.toolCalls.delete(id);
5603
+ for (const [id, run] of this.runs) if (run.agentId === agentId) this.runs.delete(id);
5604
+ for (const [id, message] of this.messages) if (message.agentId === agentId) this.messages.delete(id);
5605
+ for (let index = this.events.length - 1; index >= 0; index -= 1) {
5606
+ const event = this.events[index];
5607
+ if (event?.agentId === agentId || event?.runId && runIds.has(event.runId)) this.events.splice(index, 1);
5608
+ }
5609
+ this.agents.delete(agentId);
5610
+ }
5361
5611
  async enqueueAgentInput(input) {
5362
5612
  return this.enqueueAgentInputState(input);
5363
5613
  }
@@ -5787,10 +6037,15 @@ import { Database } from "bun:sqlite";
5787
6037
 
5788
6038
  // src/runtime/runtime-schema.ts
5789
6039
  var RUNTIME_SCHEMA_SQL = `
6040
+ CREATE TABLE IF NOT EXISTS runtime_schema (
6041
+ version INTEGER PRIMARY KEY NOT NULL
6042
+ );
5790
6043
  CREATE TABLE IF NOT EXISTS agents (
5791
6044
  id TEXT PRIMARY KEY NOT NULL,
5792
6045
  session_id TEXT NOT NULL,
6046
+ parent_session_id TEXT,
5793
6047
  state TEXT NOT NULL CHECK (state IN ('active', 'paused')),
6048
+ lifecycle_state TEXT NOT NULL CHECK (lifecycle_state IN ('active', 'archived', 'deleting')),
5794
6049
  created_at TEXT NOT NULL,
5795
6050
  updated_at TEXT NOT NULL
5796
6051
  );
@@ -5873,6 +6128,8 @@ var RUNTIME_SCHEMA_SQL = `
5873
6128
 
5874
6129
  CREATE INDEX IF NOT EXISTS agent_runs_runnable_idx
5875
6130
  ON agent_runs (state, created_at);
6131
+ CREATE UNIQUE INDEX IF NOT EXISTS agents_session_id_unique
6132
+ ON agents (session_id);
5876
6133
  `;
5877
6134
  function initializeRuntimeSchema(database) {
5878
6135
  const initialize = database.transaction(() => {
@@ -5889,6 +6146,7 @@ function initializeRuntimeSchema(database) {
5889
6146
  }
5890
6147
  });
5891
6148
  initialize();
6149
+ database.run("INSERT OR IGNORE INTO runtime_schema (version) VALUES (2)");
5892
6150
  }
5893
6151
 
5894
6152
  // src/runtime/sqlite-store.ts
@@ -5932,13 +6190,15 @@ var SqliteRuntimeStateStore = class {
5932
6190
  id: createId("agt"),
5933
6191
  sessionId: input.sessionId,
5934
6192
  state: "active",
6193
+ lifecycleState: "active",
6194
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
5935
6195
  createdAt: timestamp,
5936
6196
  updatedAt: timestamp
5937
6197
  };
5938
6198
  const create = this.database.transaction(() => {
5939
6199
  this.database.run(
5940
- "INSERT INTO agents (id, session_id, state, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
5941
- [agent.id, agent.sessionId, agent.state, agent.createdAt, agent.updatedAt]
6200
+ "INSERT INTO agents (id, session_id, parent_session_id, state, lifecycle_state, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
6201
+ [agent.id, agent.sessionId, agent.parentSessionId ?? null, agent.state, agent.lifecycleState, agent.createdAt, agent.updatedAt]
5942
6202
  );
5943
6203
  this.recordEvent("agent_created", { agentId: agent.id });
5944
6204
  });
@@ -5949,6 +6209,10 @@ var SqliteRuntimeStateStore = class {
5949
6209
  const row = this.database.query("SELECT * FROM agents WHERE id = ?").get(agentId);
5950
6210
  return row ? this.agentFromRow(row) : void 0;
5951
6211
  }
6212
+ async getAgentBySessionId(sessionId) {
6213
+ const row = this.database.query("SELECT * FROM agents WHERE session_id = ?").get(sessionId);
6214
+ return row ? this.agentFromRow(row) : void 0;
6215
+ }
5952
6216
  async listAgents() {
5953
6217
  const rows = this.database.query("SELECT * FROM agents ORDER BY created_at, id").all();
5954
6218
  return rows.map((row) => this.agentFromRow(row));
@@ -5964,6 +6228,34 @@ var SqliteRuntimeStateStore = class {
5964
6228
  })();
5965
6229
  return clone3(updated);
5966
6230
  }
6231
+ async setAgentLifecycleState(agentId, state) {
6232
+ const current = this.requireAgent(agentId);
6233
+ if (current.lifecycleState === state) return clone3(current);
6234
+ const { timestamp } = timestampFor();
6235
+ const updated = this.database.transaction(() => {
6236
+ this.database.run("UPDATE agents SET lifecycle_state = ?, updated_at = ? WHERE id = ?", [state, timestamp, agentId]);
6237
+ if (state === "archived") this.recordEvent("session_archived", { agentId });
6238
+ if (state === "active" && current.lifecycleState === "archived") this.recordEvent("session_unarchived", { agentId });
6239
+ return this.requireAgent(agentId);
6240
+ })();
6241
+ return clone3(updated);
6242
+ }
6243
+ async deleteAgentData(agentId) {
6244
+ this.database.transaction(() => {
6245
+ const agent = this.requireAgent(agentId);
6246
+ const runIds = this.database.query("SELECT id FROM agent_runs WHERE agent_id = ?").all(agentId).map((row) => row.id);
6247
+ if (runIds.length > 0) {
6248
+ const marks = runIds.map(() => "?").join(",");
6249
+ this.database.run(`DELETE FROM runtime_tool_calls WHERE run_id IN (${marks})`, runIds);
6250
+ this.database.run(`DELETE FROM runtime_leases WHERE run_id IN (${marks})`, runIds);
6251
+ this.database.run(`DELETE FROM runtime_events WHERE agent_id = ? OR run_id IN (${marks})`, [agentId, ...runIds]);
6252
+ this.database.run(`DELETE FROM agent_runs WHERE id IN (${marks})`, runIds);
6253
+ }
6254
+ this.database.run("DELETE FROM runtime_messages WHERE agent_id = ?", [agentId]);
6255
+ this.database.run("DELETE FROM runtime_events WHERE agent_id = ?", [agentId]);
6256
+ this.database.run("DELETE FROM agents WHERE id = ?", [agentId]);
6257
+ })();
6258
+ }
5967
6259
  async enqueueAgentInput(input) {
5968
6260
  const enqueue = this.database.transaction(() => this.enqueueAgentInputState(input));
5969
6261
  return clone3(enqueue());
@@ -6453,6 +6745,8 @@ var SqliteRuntimeStateStore = class {
6453
6745
  id: row.id,
6454
6746
  sessionId: row.session_id,
6455
6747
  state: row.state,
6748
+ lifecycleState: row.lifecycle_state,
6749
+ ...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
6456
6750
  createdAt: row.created_at,
6457
6751
  updatedAt: row.updated_at
6458
6752
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowan-agent/agent",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,9 +23,10 @@
23
23
  "build": "tsup && bun scripts/check-public-interface.ts"
24
24
  },
25
25
  "dependencies": {
26
- "@rowan-agent/models": "0.6.2",
26
+ "@rowan-agent/models": "0.6.3",
27
27
  "jiti": "2.7.0",
28
- "typebox": "^1.0.0"
28
+ "typebox": "^1.0.0",
29
+ "yaml": "^2.7.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "tsup": "^8.5.1"