@rowan-agent/agent 0.6.2 → 0.7.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 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 |
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" })),
@@ -1444,12 +1522,12 @@ function injectPhaseContent(phase, output, messages, mirror) {
1444
1522
  try {
1445
1523
  const phaseContent = phase.filePath ? readPhaseContent(phase) : phase.content ?? phase.description ?? "";
1446
1524
  const content = buildPhaseDirectiveMessage(
1447
- { name: phase.id, content: phaseContent },
1525
+ { name: phase.name, content: phaseContent },
1448
1526
  output
1449
1527
  );
1450
1528
  const message = createMessage("user", content, {
1451
1529
  kind: "phase_prompt",
1452
- phase: phase.id
1530
+ phase: phase.name
1453
1531
  });
1454
1532
  messages.push(message);
1455
1533
  if (mirror && mirror !== messages) mirror.push(message);
@@ -1594,7 +1672,7 @@ async function executePhaseWithModel(ctx) {
1594
1672
  for (const toolCall of collected.toolCalls) {
1595
1673
  if (toolCall.name !== PhaseRouteTool) continue;
1596
1674
  const messageId = ctx.messageManager.start("tool", createRouteToolResultContent(toolCall), {
1597
- phase: ctx.phase.id
1675
+ phase: ctx.phase.name
1598
1676
  });
1599
1677
  await ctx.messageManager.end(messageId);
1600
1678
  }
@@ -1607,7 +1685,7 @@ async function executePhaseWithModel(ctx) {
1607
1685
  for (const toolCall of executableToolCalls) {
1608
1686
  const result = await ctx.execution.executeTool(roundContext, toolCall);
1609
1687
  const messageId = ctx.messageManager.start("tool", createToolResultContent(result), {
1610
- phase: ctx.phase.id
1688
+ phase: ctx.phase.name
1611
1689
  });
1612
1690
  await ctx.messageManager.end(messageId);
1613
1691
  }
@@ -1646,7 +1724,7 @@ async function runPhaseLoop(config, state, registry) {
1646
1724
  while (currentPhaseId) {
1647
1725
  const availablePhases = [];
1648
1726
  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 });
1727
+ availablePhases.push({ name: phase2.name, description: phase2.description, tools: phase2.tools, skills: phase2.skills, input: phase2.input, isolated: phase2.isolated });
1650
1728
  }
1651
1729
  const abortResult = LoopGuard.checkAbort(config.signal);
1652
1730
  if (abortResult.stopReason !== "none") {
@@ -1759,7 +1837,7 @@ async function runPhaseLoop(config, state, registry) {
1759
1837
  } else if (latestUserInputMessage) {
1760
1838
  const phaseInputMessage = createMessage("user", latestUserInputMessage.content, {
1761
1839
  kind: "phase_input",
1762
- phase: phase.id
1840
+ phase: phase.name
1763
1841
  });
1764
1842
  config.context.messages.push(phaseInputMessage);
1765
1843
  phaseContext.messages.push(phaseInputMessage);
@@ -1931,7 +2009,7 @@ async function runPhaseLoop(config, state, registry) {
1931
2009
  ts: createTimestamp()
1932
2010
  });
1933
2011
  previousPayload = output.payload;
1934
- previousResults = output.payload !== void 0 ? [{ name: phase.id, output: output.payload }] : [];
2012
+ previousResults = output.payload !== void 0 ? [{ name: phase.name, output: output.payload }] : [];
1935
2013
  currentPhaseId = targetPhaseId;
1936
2014
  }
1937
2015
  throw new Error("Phase machine exited without a stop or abort transition.");
@@ -2084,7 +2162,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2084
2162
  },
2085
2163
  async invokeModel(phaseContext) {
2086
2164
  if (config.beforePrompt) {
2087
- phaseContext = await config.beforePrompt(phase.id, phaseContext);
2165
+ phaseContext = await config.beforePrompt(phase.name, phaseContext);
2088
2166
  }
2089
2167
  const request = buildModelRequest({
2090
2168
  systemPrompt: phaseContext.systemPrompt,
@@ -2111,7 +2189,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2111
2189
  config,
2112
2190
  message: messageManager,
2113
2191
  request,
2114
- phaseId: phase.id
2192
+ phaseId: phase.name
2115
2193
  }),
2116
2194
  {
2117
2195
  signal: config.signal,
@@ -2133,7 +2211,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2133
2211
  }
2134
2212
  )
2135
2213
  );
2136
- await config.onModelTranscript?.(result.transcript, { phase: phase.id, model: phase.model ?? config.model });
2214
+ await config.onModelTranscript?.(result.transcript, { phase: phase.name, model: phase.model ?? config.model });
2137
2215
  return result;
2138
2216
  },
2139
2217
  async executeTool(_context, toolCall) {
@@ -2166,7 +2244,7 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2166
2244
  sourcePhaseId
2167
2245
  },
2168
2246
  state: {
2169
- current: phase.id,
2247
+ current: phase.name,
2170
2248
  available: Array.from(registry.phases.keys()),
2171
2249
  iterations: 0,
2172
2250
  payload
@@ -2185,7 +2263,7 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2185
2263
  if (phaseMsgId) removePhaseMessage(messages, phaseMsgId);
2186
2264
  const decision = output.toolCalls ? extractRouteCall(output.toolCalls) : void 0;
2187
2265
  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 };
2266
+ return { instanceId, phaseId: phase.name, payload: resultPayload, content: output.message };
2189
2267
  }
2190
2268
  async function waitForBackgroundTasks(backgroundTasks) {
2191
2269
  const entries = Array.from(backgroundTasks.entries());
@@ -2273,23 +2351,55 @@ function readSkillContent(skill) {
2273
2351
  }
2274
2352
  async function loadSkill(path) {
2275
2353
  const resolved = resolveSkillPath(path);
2276
- const { frontmatter, body } = await loadMarkdown(resolved);
2354
+ let loaded;
2355
+ try {
2356
+ loaded = await loadMarkdown(resolved);
2357
+ } catch (error) {
2358
+ if (!(error instanceof FrontmatterParseError)) throw error;
2359
+ warnResourceDiagnostics("skill", resolved, [`frontmatter could not be parsed: ${error.message}`]);
2360
+ throw new ResourceMetadataError("parse_failed", error.message);
2361
+ }
2362
+ const { frontmatter, body } = loaded;
2363
+ const metadata = frontmatter;
2364
+ const id = inferResourceName(resolved, SKILL_MARKER);
2365
+ const frontmatterName = typeof metadata.name === "string" ? metadata.name : void 0;
2366
+ const name = frontmatterName || id;
2367
+ const description = validateDescription(metadata.description);
2368
+ const diagnostics = validateResourceId(id);
2369
+ if (frontmatterName && frontmatterName !== id) {
2370
+ diagnostics.push(...validateResourceName(frontmatterName, id));
2371
+ }
2372
+ diagnostics.push(...description.warnings);
2373
+ warnResourceDiagnostics("skill", resolved, diagnostics);
2374
+ if (description.missing) {
2375
+ throw new ResourceMetadataError("invalid_metadata", "description is required");
2376
+ }
2277
2377
  return {
2278
- name: frontmatter.name ?? inferResourceName(resolved, SKILL_MARKER),
2279
- description: frontmatter.description ?? "",
2378
+ name,
2379
+ description: description.description,
2280
2380
  filePath: resolved,
2281
2381
  baseDir: dirname4(resolved),
2282
2382
  content: body,
2283
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
2383
+ disableModelInvocation: metadata["disable-model-invocation"] === true
2284
2384
  };
2285
2385
  }
2286
2386
  async function loadSkills(targetPath) {
2287
2387
  const skillsDir = resolve4(targetPath);
2288
2388
  if (existsSync2(skillsDir) && statSync2(skillsDir).isFile()) {
2289
- return [await loadSkill(skillsDir)];
2389
+ try {
2390
+ return [await loadSkill(skillsDir)];
2391
+ } catch (error) {
2392
+ if (error instanceof ResourceMetadataError) return [];
2393
+ throw error;
2394
+ }
2290
2395
  }
2291
2396
  if (existsSync2(join2(skillsDir, SKILL_MARKER))) {
2292
- return [await loadSkill(skillsDir)];
2397
+ try {
2398
+ return [await loadSkill(skillsDir)];
2399
+ } catch (error) {
2400
+ if (error instanceof ResourceMetadataError) return [];
2401
+ throw error;
2402
+ }
2293
2403
  }
2294
2404
  const entries = await readdir2(skillsDir, { withFileTypes: true });
2295
2405
  const skills = [];
@@ -2301,6 +2411,7 @@ async function loadSkills(targetPath) {
2301
2411
  const skill = await loadSkill(skillFile);
2302
2412
  skills.push(skill);
2303
2413
  } catch (error) {
2414
+ if (error instanceof ResourceMetadataError) continue;
2304
2415
  console.warn(`Failed to load skill "${entry.name}":`, error);
2305
2416
  }
2306
2417
  }
@@ -2794,8 +2905,8 @@ var ExtensionRunner = class {
2794
2905
  // ---------------------------------------------------------------------------
2795
2906
  // Phase management
2796
2907
  // ---------------------------------------------------------------------------
2797
- getPhase(id) {
2798
- const reg = this.getRegisteredPhase(id);
2908
+ getPhase(name) {
2909
+ const reg = this.getRegisteredPhase(name);
2799
2910
  if (!reg) return void 0;
2800
2911
  return this.adaptToPhase(reg);
2801
2912
  }
@@ -2807,8 +2918,8 @@ var ExtensionRunner = class {
2807
2918
  createPhaseRegistry(input = {}) {
2808
2919
  const registered = this.collectRegisteredPhases();
2809
2920
  const phases = /* @__PURE__ */ new Map();
2810
- for (const [id, reg] of registered) {
2811
- phases.set(id, this.adaptToPhase(reg));
2921
+ for (const [name, reg] of registered) {
2922
+ phases.set(name, this.adaptToPhase(reg));
2812
2923
  }
2813
2924
  const entryPhaseId = input.entryPhaseId ?? null;
2814
2925
  return { phases, entryPhaseId };
@@ -2817,9 +2928,8 @@ var ExtensionRunner = class {
2817
2928
  adaptToPhase(reg) {
2818
2929
  const def = reg.definition;
2819
2930
  return {
2820
- id: def.id,
2821
- name: def.name ?? def.id,
2822
- description: def.description ?? "",
2931
+ name: def.name,
2932
+ description: def.description,
2823
2933
  tools: def.tools,
2824
2934
  skills: def.skills,
2825
2935
  target: def.target,
@@ -3086,22 +3196,27 @@ var ExtensionRunner = class {
3086
3196
  });
3087
3197
  }
3088
3198
  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.`);
3199
+ if (typeof registration.name !== "string" || registration.name.length === 0) {
3200
+ throw new Error(`Phase registration requires a "name" field.`);
3201
+ }
3202
+ const name = registration.name;
3203
+ const description = validateDescription(registration.description);
3204
+ const errors = validateResourceId(name, "name");
3205
+ if (description.missing) errors.push(...description.warnings);
3206
+ errors.push(...validateSkillReferences(registration.skills));
3207
+ errors.push(...validatePhaseTarget(registration.target));
3208
+ if (errors.length > 0) {
3209
+ throw new Error(`Invalid phase registration "${name}": ${errors.join("; ")}`);
3094
3210
  }
3095
- if (!registration.description) {
3096
- throw new Error(`Phase registration "${registration.id}" requires a "description" field.`);
3211
+ if (description.warnings.length > 0) {
3212
+ warnResourceDiagnostics("phase", `extension ${extension.path}`, description.warnings);
3097
3213
  }
3098
- if (this.phases.has(registration.id)) {
3099
- throw new Error(`Duplicate phase id: ${registration.id}`);
3214
+ if (this.phases.has(name)) {
3215
+ throw new Error(`Duplicate phase name: ${name}`);
3100
3216
  }
3101
3217
  const definition = {
3102
- id: registration.id,
3103
- name: registration.name,
3104
- description: registration.description,
3218
+ name,
3219
+ description: description.description,
3105
3220
  run: registration.run,
3106
3221
  ...registration.model ? { model: registration.model } : {}
3107
3222
  };
@@ -3109,8 +3224,8 @@ var ExtensionRunner = class {
3109
3224
  definition,
3110
3225
  source: { extensionPath: extension.path }
3111
3226
  };
3112
- this.phases.set(registration.id, registered);
3113
- extension.phases.set(registration.id, registered);
3227
+ this.phases.set(name, registered);
3228
+ extension.phases.set(name, registered);
3114
3229
  this._phaseCache = null;
3115
3230
  }
3116
3231
  registerProvider(config) {
@@ -3137,8 +3252,8 @@ var ExtensionRunner = class {
3137
3252
  }
3138
3253
  this.pendingProviders.length = 0;
3139
3254
  }
3140
- getRegisteredPhase(id) {
3141
- return this.collectRegisteredPhases().get(id);
3255
+ getRegisteredPhase(name) {
3256
+ return this.collectRegisteredPhases().get(name);
3142
3257
  }
3143
3258
  collectRegisteredPhases() {
3144
3259
  if (this._phaseCache) return this._phaseCache;
@@ -4472,6 +4587,22 @@ var InMemorySessionStore = class {
4472
4587
  async open(sessionId) {
4473
4588
  return this.sessions.get(sessionId);
4474
4589
  }
4590
+ async list() {
4591
+ return Promise.all([...this.sessions.values()].map(async (session) => {
4592
+ const header = await session.getHeader();
4593
+ const entries = "listEntries" in session && typeof session.listEntries === "function" ? await session.listEntries() : [];
4594
+ return {
4595
+ id: header.id,
4596
+ title: header.title,
4597
+ createdAt: header.createdAt,
4598
+ updatedAt: header.updatedAt,
4599
+ messageCount: entries.filter((entry) => entry.type === "message").length
4600
+ };
4601
+ }));
4602
+ }
4603
+ async delete(sessionId) {
4604
+ return this.sessions.delete(sessionId);
4605
+ }
4475
4606
  };
4476
4607
  var JsonlSessionStore = class {
4477
4608
  constructor(sessionsDir) {
@@ -4487,6 +4618,9 @@ var JsonlSessionStore = class {
4487
4618
  list() {
4488
4619
  return LocalJsonlSessionManager.list(this.sessionsDir);
4489
4620
  }
4621
+ delete(sessionId) {
4622
+ return LocalJsonlSessionManager.delete(this.sessionsDir, sessionId);
4623
+ }
4490
4624
  };
4491
4625
 
4492
4626
  // src/runtime/agent-runtime.ts
@@ -4762,6 +4896,7 @@ var AgentRuntime = class _AgentRuntime {
4762
4896
  toolRuntime;
4763
4897
  bindings = /* @__PURE__ */ new Map();
4764
4898
  agents = /* @__PURE__ */ new Map();
4899
+ invalidatedAgents = /* @__PURE__ */ new Set();
4765
4900
  runHandles = /* @__PURE__ */ new Map();
4766
4901
  runWaiters = /* @__PURE__ */ new Map();
4767
4902
  pendingRuns = [];
@@ -4808,6 +4943,15 @@ var AgentRuntime = class _AgentRuntime {
4808
4943
  const runtime = new _AgentRuntime(options);
4809
4944
  activeRuntime = runtime;
4810
4945
  try {
4946
+ if (options.sessionProvider) {
4947
+ for (const record of await options.stateStore.listAgents()) {
4948
+ if (record.lifecycleState !== "deleting") continue;
4949
+ const activeRuns = await options.stateStore.listRuns({ agentId: record.id, states: ["queued", "running", "suspended"] });
4950
+ if (activeRuns.length > 0) continue;
4951
+ await options.stateStore.deleteAgentData(record.id);
4952
+ await options.sessionProvider.delete(record.sessionId);
4953
+ }
4954
+ }
4811
4955
  await runtime.recover();
4812
4956
  runtime.startLeaseRecovery();
4813
4957
  return runtime;
@@ -4826,8 +4970,10 @@ var AgentRuntime = class _AgentRuntime {
4826
4970
  input: "",
4827
4971
  skills: options.context.skills
4828
4972
  });
4973
+ const header = await manager.getHeader();
4829
4974
  const record = await this.stateStore.createAgent({
4830
- sessionId: manager.getSessionId()
4975
+ sessionId: manager.getSessionId(),
4976
+ parentSessionId: header.parentSessionId
4831
4977
  });
4832
4978
  this.pausedAgents.delete(record.id);
4833
4979
  const agent = await this.attachAgent(record, manager, options);
@@ -4839,6 +4985,7 @@ var AgentRuntime = class _AgentRuntime {
4839
4985
  assertAgentOptions(options);
4840
4986
  const record = await this.stateStore.getAgent(agentId);
4841
4987
  if (!record) throw new Error(`Agent not found: ${agentId}.`);
4988
+ if (record.lifecycleState !== "active") throw new Error(`Session ${record.sessionId} is ${record.lifecycleState}.`);
4842
4989
  if (record.state === "paused") this.pausedAgents.add(record.id);
4843
4990
  else this.pausedAgents.delete(record.id);
4844
4991
  const provider = this.sessionProvider;
@@ -4947,6 +5094,82 @@ var AgentRuntime = class _AgentRuntime {
4947
5094
  async listRuns(input) {
4948
5095
  return this.stateStore.listRuns(input);
4949
5096
  }
5097
+ async listSessions(input = {}) {
5098
+ const records = await this.stateStore.listAgents();
5099
+ const items = this.sessionProvider ? await this.sessionProvider.list() : [];
5100
+ const summaries = new Map(items.map((item) => [item.id, item]));
5101
+ const activeRuns = await this.stateStore.listActiveRuns();
5102
+ return records.filter((record) => !input.state || record.lifecycleState === input.state).map((record) => {
5103
+ const item = summaries.get(record.sessionId);
5104
+ return {
5105
+ id: record.sessionId,
5106
+ state: record.lifecycleState,
5107
+ createdAt: record.createdAt,
5108
+ updatedAt: item?.updatedAt ?? record.updatedAt,
5109
+ ...record.lifecycleState === "archived" ? { archivedAt: record.updatedAt } : {},
5110
+ ...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
5111
+ agentId: record.id,
5112
+ activeRunCount: activeRuns.filter((run) => run.agentId === record.id).length,
5113
+ ...item?.title ? { title: item.title } : {}
5114
+ };
5115
+ });
5116
+ }
5117
+ async archiveSession(sessionId) {
5118
+ const record = await this.requireSessionAgent(sessionId);
5119
+ if (record.lifecycleState === "archived") return;
5120
+ if (record.lifecycleState === "deleting") throw new Error(`Session ${sessionId} is being deleted.`);
5121
+ await this.stateStore.setAgentLifecycleState(record.id, "archived");
5122
+ this.publishEvents();
5123
+ }
5124
+ async unarchiveSession(sessionId) {
5125
+ const record = await this.requireSessionAgent(sessionId);
5126
+ if (record.lifecycleState === "active") return;
5127
+ if (record.lifecycleState === "deleting") throw new Error(`Session ${sessionId} is being deleted.`);
5128
+ await this.stateStore.setAgentLifecycleState(record.id, "active");
5129
+ await this.scheduleQueuedRuns(record.id);
5130
+ this.publishEvents();
5131
+ }
5132
+ async deleteSession(sessionId) {
5133
+ this.assertRunning();
5134
+ if (!this.sessionProvider) throw new Error("Agent Runtime requires a SessionManager provider to delete a Session.");
5135
+ if (!this.sessionProvider.delete) throw new Error("Session Manager provider does not support deletion.");
5136
+ const root = await this.requireSessionAgent(sessionId);
5137
+ const records = await this.stateStore.listAgents();
5138
+ const subtree = /* @__PURE__ */ new Set([root.id]);
5139
+ let changed = true;
5140
+ while (changed) {
5141
+ changed = false;
5142
+ for (const record of records) {
5143
+ if (record.parentSessionId && records.some((candidate) => subtree.has(candidate.id) && candidate.sessionId === record.parentSessionId) && !subtree.has(record.id)) {
5144
+ subtree.add(record.id);
5145
+ changed = true;
5146
+ }
5147
+ }
5148
+ }
5149
+ const activeRuns = await this.stateStore.listActiveRuns();
5150
+ if (activeRuns.some((run) => subtree.has(run.agentId))) {
5151
+ throw new Error(`Session ${sessionId} has active Runs and cannot be deleted.`);
5152
+ }
5153
+ const targets = records.filter((record) => subtree.has(record.id));
5154
+ for (const record of targets) await this.stateStore.setAgentLifecycleState(record.id, "deleting");
5155
+ for (const record of targets) {
5156
+ this.bindings.delete(record.id);
5157
+ this.agents.delete(record.id);
5158
+ this.invalidatedAgents.add(record.id);
5159
+ this.pausedAgents.delete(record.id);
5160
+ for (let index = this.pendingRuns.length - 1; index >= 0; index -= 1) {
5161
+ if (this.pendingRuns[index]?.agentId === record.id) this.pendingRuns.splice(index, 1);
5162
+ }
5163
+ await this.stateStore.deleteAgentData(record.id);
5164
+ await this.sessionProvider.delete(record.sessionId);
5165
+ }
5166
+ this.publishEvents();
5167
+ }
5168
+ async requireSessionAgent(sessionId) {
5169
+ const record = await this.stateStore.getAgentBySessionId(sessionId);
5170
+ if (!record) throw new Error(`Session not found: ${sessionId}.`);
5171
+ return record;
5172
+ }
4950
5173
  getAgent(agentId) {
4951
5174
  return this.agents.get(agentId);
4952
5175
  }
@@ -5079,7 +5302,11 @@ var AgentRuntime = class _AgentRuntime {
5079
5302
  }
5080
5303
  async submitAgentInput(agentId, input, persist) {
5081
5304
  this.assertRunning();
5305
+ if (this.invalidatedAgents.has(agentId)) throw new Error(`Session for Agent ${agentId} was deleted.`);
5082
5306
  if (!this.bindings.has(agentId)) throw new Error(`Agent ${agentId} has no live Agent Binding.`);
5307
+ const record = await this.stateStore.getAgent(agentId);
5308
+ if (!record) throw new Error(`Agent ${agentId} was deleted.`);
5309
+ if (record.lifecycleState !== "active") throw new Error(`Session ${record.sessionId} is ${record.lifecycleState}.`);
5083
5310
  const enqueued = await this.stateStore.enqueueAgentInput({ agentId, input });
5084
5311
  await persist();
5085
5312
  if (enqueued.resumed) this.notifyRun(enqueued.run);
@@ -5335,6 +5562,8 @@ var InMemoryRuntimeStateStore = class {
5335
5562
  id: createId("agt"),
5336
5563
  sessionId: input.sessionId,
5337
5564
  state: "active",
5565
+ lifecycleState: "active",
5566
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
5338
5567
  createdAt: timestamp,
5339
5568
  updatedAt: timestamp
5340
5569
  };
@@ -5346,6 +5575,10 @@ var InMemoryRuntimeStateStore = class {
5346
5575
  const agent = this.agents.get(agentId);
5347
5576
  return agent ? clone2(agent) : void 0;
5348
5577
  }
5578
+ async getAgentBySessionId(sessionId) {
5579
+ const agent = [...this.agents.values()].find((candidate) => candidate.sessionId === sessionId);
5580
+ return agent ? clone2(agent) : void 0;
5581
+ }
5349
5582
  async listAgents() {
5350
5583
  return clone2([...this.agents.values()]);
5351
5584
  }
@@ -5358,6 +5591,27 @@ var InMemoryRuntimeStateStore = class {
5358
5591
  this.recordEvent(state === "paused" ? "agent_paused" : "agent_resumed", { agentId });
5359
5592
  return clone2(agent);
5360
5593
  }
5594
+ async setAgentLifecycleState(agentId, state) {
5595
+ const agent = this.requireAgent(agentId);
5596
+ if (agent.lifecycleState === state) return clone2(agent);
5597
+ agent.lifecycleState = state;
5598
+ agent.updatedAt = createTimestamp();
5599
+ if (state === "archived") this.recordEvent("session_archived", { agentId });
5600
+ if (state === "active") this.recordEvent("session_unarchived", { agentId });
5601
+ return clone2(agent);
5602
+ }
5603
+ async deleteAgentData(agentId) {
5604
+ this.requireAgent(agentId);
5605
+ const runIds = new Set([...this.runs.values()].filter((run) => run.agentId === agentId).map((run) => run.id));
5606
+ for (const [id, call] of this.toolCalls) if (call.agentId === agentId || runIds.has(call.runId)) this.toolCalls.delete(id);
5607
+ for (const [id, run] of this.runs) if (run.agentId === agentId) this.runs.delete(id);
5608
+ for (const [id, message] of this.messages) if (message.agentId === agentId) this.messages.delete(id);
5609
+ for (let index = this.events.length - 1; index >= 0; index -= 1) {
5610
+ const event = this.events[index];
5611
+ if (event?.agentId === agentId || event?.runId && runIds.has(event.runId)) this.events.splice(index, 1);
5612
+ }
5613
+ this.agents.delete(agentId);
5614
+ }
5361
5615
  async enqueueAgentInput(input) {
5362
5616
  return this.enqueueAgentInputState(input);
5363
5617
  }
@@ -5787,10 +6041,15 @@ import { Database } from "bun:sqlite";
5787
6041
 
5788
6042
  // src/runtime/runtime-schema.ts
5789
6043
  var RUNTIME_SCHEMA_SQL = `
6044
+ CREATE TABLE IF NOT EXISTS runtime_schema (
6045
+ version INTEGER PRIMARY KEY NOT NULL
6046
+ );
5790
6047
  CREATE TABLE IF NOT EXISTS agents (
5791
6048
  id TEXT PRIMARY KEY NOT NULL,
5792
6049
  session_id TEXT NOT NULL,
6050
+ parent_session_id TEXT,
5793
6051
  state TEXT NOT NULL CHECK (state IN ('active', 'paused')),
6052
+ lifecycle_state TEXT NOT NULL CHECK (lifecycle_state IN ('active', 'archived', 'deleting')),
5794
6053
  created_at TEXT NOT NULL,
5795
6054
  updated_at TEXT NOT NULL
5796
6055
  );
@@ -5873,6 +6132,8 @@ var RUNTIME_SCHEMA_SQL = `
5873
6132
 
5874
6133
  CREATE INDEX IF NOT EXISTS agent_runs_runnable_idx
5875
6134
  ON agent_runs (state, created_at);
6135
+ CREATE UNIQUE INDEX IF NOT EXISTS agents_session_id_unique
6136
+ ON agents (session_id);
5876
6137
  `;
5877
6138
  function initializeRuntimeSchema(database) {
5878
6139
  const initialize = database.transaction(() => {
@@ -5889,6 +6150,7 @@ function initializeRuntimeSchema(database) {
5889
6150
  }
5890
6151
  });
5891
6152
  initialize();
6153
+ database.run("INSERT OR IGNORE INTO runtime_schema (version) VALUES (2)");
5892
6154
  }
5893
6155
 
5894
6156
  // src/runtime/sqlite-store.ts
@@ -5932,13 +6194,15 @@ var SqliteRuntimeStateStore = class {
5932
6194
  id: createId("agt"),
5933
6195
  sessionId: input.sessionId,
5934
6196
  state: "active",
6197
+ lifecycleState: "active",
6198
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
5935
6199
  createdAt: timestamp,
5936
6200
  updatedAt: timestamp
5937
6201
  };
5938
6202
  const create = this.database.transaction(() => {
5939
6203
  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]
6204
+ "INSERT INTO agents (id, session_id, parent_session_id, state, lifecycle_state, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
6205
+ [agent.id, agent.sessionId, agent.parentSessionId ?? null, agent.state, agent.lifecycleState, agent.createdAt, agent.updatedAt]
5942
6206
  );
5943
6207
  this.recordEvent("agent_created", { agentId: agent.id });
5944
6208
  });
@@ -5949,6 +6213,10 @@ var SqliteRuntimeStateStore = class {
5949
6213
  const row = this.database.query("SELECT * FROM agents WHERE id = ?").get(agentId);
5950
6214
  return row ? this.agentFromRow(row) : void 0;
5951
6215
  }
6216
+ async getAgentBySessionId(sessionId) {
6217
+ const row = this.database.query("SELECT * FROM agents WHERE session_id = ?").get(sessionId);
6218
+ return row ? this.agentFromRow(row) : void 0;
6219
+ }
5952
6220
  async listAgents() {
5953
6221
  const rows = this.database.query("SELECT * FROM agents ORDER BY created_at, id").all();
5954
6222
  return rows.map((row) => this.agentFromRow(row));
@@ -5964,6 +6232,34 @@ var SqliteRuntimeStateStore = class {
5964
6232
  })();
5965
6233
  return clone3(updated);
5966
6234
  }
6235
+ async setAgentLifecycleState(agentId, state) {
6236
+ const current = this.requireAgent(agentId);
6237
+ if (current.lifecycleState === state) return clone3(current);
6238
+ const { timestamp } = timestampFor();
6239
+ const updated = this.database.transaction(() => {
6240
+ this.database.run("UPDATE agents SET lifecycle_state = ?, updated_at = ? WHERE id = ?", [state, timestamp, agentId]);
6241
+ if (state === "archived") this.recordEvent("session_archived", { agentId });
6242
+ if (state === "active" && current.lifecycleState === "archived") this.recordEvent("session_unarchived", { agentId });
6243
+ return this.requireAgent(agentId);
6244
+ })();
6245
+ return clone3(updated);
6246
+ }
6247
+ async deleteAgentData(agentId) {
6248
+ this.database.transaction(() => {
6249
+ const agent = this.requireAgent(agentId);
6250
+ const runIds = this.database.query("SELECT id FROM agent_runs WHERE agent_id = ?").all(agentId).map((row) => row.id);
6251
+ if (runIds.length > 0) {
6252
+ const marks = runIds.map(() => "?").join(",");
6253
+ this.database.run(`DELETE FROM runtime_tool_calls WHERE run_id IN (${marks})`, runIds);
6254
+ this.database.run(`DELETE FROM runtime_leases WHERE run_id IN (${marks})`, runIds);
6255
+ this.database.run(`DELETE FROM runtime_events WHERE agent_id = ? OR run_id IN (${marks})`, [agentId, ...runIds]);
6256
+ this.database.run(`DELETE FROM agent_runs WHERE id IN (${marks})`, runIds);
6257
+ }
6258
+ this.database.run("DELETE FROM runtime_messages WHERE agent_id = ?", [agentId]);
6259
+ this.database.run("DELETE FROM runtime_events WHERE agent_id = ?", [agentId]);
6260
+ this.database.run("DELETE FROM agents WHERE id = ?", [agentId]);
6261
+ })();
6262
+ }
5967
6263
  async enqueueAgentInput(input) {
5968
6264
  const enqueue = this.database.transaction(() => this.enqueueAgentInputState(input));
5969
6265
  return clone3(enqueue());
@@ -6453,6 +6749,8 @@ var SqliteRuntimeStateStore = class {
6453
6749
  id: row.id,
6454
6750
  sessionId: row.session_id,
6455
6751
  state: row.state,
6752
+ lifecycleState: row.lifecycle_state,
6753
+ ...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
6456
6754
  createdAt: row.created_at,
6457
6755
  updatedAt: row.updated_at
6458
6756
  };
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.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,8 @@
25
25
  "dependencies": {
26
26
  "@rowan-agent/models": "0.6.2",
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"