@vimhead.dev/norn-cli 0.1.0-tip.35614826665.1 → 0.1.0-tip.35724621678.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.
Files changed (119) hide show
  1. package/assets/README.md +41 -13
  2. package/assets/docs/README.md +16 -16
  3. package/assets/docs/agents.md +23 -15
  4. package/assets/docs/cli.md +10 -8
  5. package/assets/docs/composition.md +7 -7
  6. package/assets/docs/persistence.md +22 -22
  7. package/assets/docs/projects.md +6 -6
  8. package/assets/docs/recovery.md +10 -10
  9. package/assets/docs/schemas.md +42 -36
  10. package/assets/docs/workflows.md +39 -32
  11. package/assets/examples/agent-then-analysis/input.json +3 -3
  12. package/assets/examples/agent-then-analysis/norn.project.json +2 -2
  13. package/assets/examples/agent-then-analysis/plugin.ts +37 -9
  14. package/assets/examples/caller-owned-routing/README.md +6 -6
  15. package/assets/examples/caller-owned-routing/assessment.ts +18 -4
  16. package/assets/examples/caller-owned-routing/contracts.ts +7 -2
  17. package/assets/examples/caller-owned-routing/input.json +12 -12
  18. package/assets/examples/caller-owned-routing/norn.project.json +2 -2
  19. package/assets/examples/caller-owned-routing/revision.ts +7 -2
  20. package/assets/examples/caller-owned-routing/router.ts +14 -3
  21. package/assets/examples/caller-selected-continuation/caller.ts +38 -12
  22. package/assets/examples/caller-selected-continuation/input.json +7 -7
  23. package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
  24. package/assets/examples/caller-selected-continuation/producer.ts +10 -3
  25. package/assets/examples/coordinating-multiple-agents/README.md +6 -6
  26. package/assets/examples/coordinating-multiple-agents/input.json +20 -8
  27. package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
  28. package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
  29. package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
  30. package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
  31. package/assets/examples/getting-started/norn.project.json +2 -2
  32. package/assets/examples/getting-started/plugin.ts +33 -13
  33. package/assets/examples/minimal-workflow/norn.project.json +2 -2
  34. package/assets/examples/minimal-workflow/plugin.ts +12 -3
  35. package/assets/examples/shared-state/README.md +6 -6
  36. package/assets/examples/shared-state/input.json +1 -1
  37. package/assets/examples/shared-state/norn.project.json +2 -2
  38. package/assets/examples/shared-state/plugin.ts +44 -15
  39. package/assets/examples/shared-state/shared-state.ts +46 -13
  40. package/assets/examples/shared-state/state-tools.ts +68 -16
  41. package/assets/examples/worktree-development-loop/README.md +12 -12
  42. package/assets/examples/worktree-development-loop/norn.project.json +7 -7
  43. package/assets/examples/worktree-development-loop/plugin.ts +7 -1
  44. package/assets/examples/worktree-development-loop/scope.ts +4 -1
  45. package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
  46. package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
  47. package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
  48. package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
  49. package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
  50. package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
  51. package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
  52. package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
  53. package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
  54. package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
  55. package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
  56. package/assets/package.json +1 -1
  57. package/assets/packages/cli/src/build-info.ts +4 -1
  58. package/assets/packages/cli/src/bun/cli.ts +21 -5
  59. package/assets/packages/cli/src/cli.ts +585 -170
  60. package/assets/packages/cli/src/client.ts +194 -36
  61. package/assets/packages/cli/src/documentation.ts +135 -30
  62. package/assets/packages/cli/src/generated-build-info.ts +2 -2
  63. package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
  64. package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
  65. package/assets/packages/cli/src/internal/agents.ts +248 -60
  66. package/assets/packages/cli/src/internal/commands.ts +82 -20
  67. package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
  68. package/assets/packages/cli/src/internal/engine.ts +396 -87
  69. package/assets/packages/cli/src/internal/errors.ts +13 -5
  70. package/assets/packages/cli/src/internal/execution-context.ts +20 -12
  71. package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
  72. package/assets/packages/cli/src/internal/file-names.ts +4 -1
  73. package/assets/packages/cli/src/internal/launch-request.ts +74 -22
  74. package/assets/packages/cli/src/internal/logs.ts +10 -2
  75. package/assets/packages/cli/src/internal/metrics.ts +247 -61
  76. package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
  77. package/assets/packages/cli/src/internal/run-lease.ts +72 -22
  78. package/assets/packages/cli/src/internal/run-log.ts +49 -20
  79. package/assets/packages/cli/src/internal/run-names.ts +120 -12
  80. package/assets/packages/cli/src/internal/run-state.ts +306 -71
  81. package/assets/packages/cli/src/internal/run-store.ts +216 -53
  82. package/assets/packages/cli/src/internal/usage.ts +23 -7
  83. package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
  84. package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
  85. package/assets/packages/cli/src/internal/working-directory.ts +2 -1
  86. package/assets/packages/cli/src/workflow-loader.ts +341 -74
  87. package/assets/packages/core/src/atomic-files.ts +13 -4
  88. package/assets/packages/core/src/workflow-transition.ts +13 -2
  89. package/assets/packages/sdk/src/api.ts +248 -80
  90. package/assets/packages/sdk/src/index.ts +6 -1
  91. package/assets/packages/sdk/src/schema.ts +75 -21
  92. package/assets/tests/workflow-ref.test.ts +242 -57
  93. package/bin/norn.mjs +13 -4
  94. package/dist/cli.js +344 -90
  95. package/dist/client.js +100 -22
  96. package/dist/documentation.js +102 -25
  97. package/dist/generated-build-info.d.ts +2 -2
  98. package/dist/generated-build-info.js +2 -2
  99. package/dist/internal/agent-directory.js +3 -1
  100. package/dist/internal/agent-response-tool.js +42 -11
  101. package/dist/internal/agents.js +149 -37
  102. package/dist/internal/commands.js +65 -16
  103. package/dist/internal/documentation-bundle.js +23 -11
  104. package/dist/internal/engine.js +304 -66
  105. package/dist/internal/errors.js +4 -2
  106. package/dist/internal/execution-context.js +2 -1
  107. package/dist/internal/file-coordinator.js +59 -19
  108. package/dist/internal/launch-request.js +32 -14
  109. package/dist/internal/metrics.js +138 -38
  110. package/dist/internal/pi-assets.js +70 -19
  111. package/dist/internal/run-lease.js +29 -9
  112. package/dist/internal/run-log.js +24 -12
  113. package/dist/internal/run-state.js +172 -48
  114. package/dist/internal/run-store.js +157 -45
  115. package/dist/internal/worker-directory.js +6 -2
  116. package/dist/internal/workflow-registry.js +123 -32
  117. package/dist/internal/working-directory.js +2 -1
  118. package/dist/workflow-loader.js +198 -41
  119. package/package.json +2 -2
@@ -1,4 +1,7 @@
1
- import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ defineTool,
3
+ type ToolDefinition,
4
+ } from "@earendil-works/pi-coding-agent";
2
5
  import type { NornDispose } from "@vimhead.dev/norn";
3
6
  import { AGENT_RESPONSE_TOOL_NAME } from "@vimhead.dev/norn-core/agent-protocol";
4
7
  import { Type, type TSchema } from "typebox";
@@ -33,13 +36,21 @@ export class NornAgentResponseCollector {
33
36
  return () => this.pending.delete(runId);
34
37
  }
35
38
 
36
- capture(runId: string, label: string, response: unknown): { label: string; response: unknown } {
39
+ capture(
40
+ runId: string,
41
+ label: string,
42
+ response: unknown,
43
+ ): { label: string; response: unknown } {
37
44
  const pending = this.pending.get(runId);
38
- if (!pending) throw new Error(`No active workflow agent response slot: ${runId}`);
45
+ if (!pending)
46
+ throw new Error(`No active workflow agent response slot: ${runId}`);
39
47
  if (pending.label !== label) {
40
- throw new Error(`Workflow agent response label mismatch: expected ${pending.label}, received ${label}`);
48
+ throw new Error(
49
+ `Workflow agent response label mismatch: expected ${pending.label}, received ${label}`,
50
+ );
41
51
  }
42
- if (pending.called) throw new Error(`Workflow agent response already recorded for ${runId}`);
52
+ if (pending.called)
53
+ throw new Error(`Workflow agent response already recorded for ${runId}`);
43
54
  const decoded = Value.Decode(pending.responseSchema, response);
44
55
  pending.called = true;
45
56
  pending.response = decoded;
@@ -48,7 +59,9 @@ export class NornAgentResponseCollector {
48
59
 
49
60
  get(runId: string): NornCapturedAgentResponse {
50
61
  const pending = this.pending.get(runId);
51
- return pending ? { called: pending.called, response: pending.response } : { called: false };
62
+ return pending
63
+ ? { called: pending.called, response: pending.response }
64
+ : { called: false };
52
65
  }
53
66
  }
54
67
 
@@ -60,23 +73,47 @@ export class NornAgentResponseToolFactory {
60
73
  return defineTool({
61
74
  name: AGENT_RESPONSE_TOOL_NAME,
62
75
  label: "Workflow Agent Response",
63
- description: "Record the final structured response for a Norn nested agent run.",
64
- promptSnippet: "Record a final structured response for Norn agent automation",
76
+ description:
77
+ "Record the final structured response for a Norn nested agent run.",
78
+ promptSnippet:
79
+ "Record a final structured response for Norn agent automation",
65
80
  promptGuidelines: [
66
81
  `Use ${AGENT_RESPONSE_TOOL_NAME} as your final action when a workflow prompt gives you a workflow response runId.`,
67
82
  `After calling ${AGENT_RESPONSE_TOOL_NAME}, do not emit another assistant response in the same turn.`,
68
83
  ],
69
84
  parameters: Type.Object({
70
- runId: Type.String({ description: "The exact workflow response runId provided in the prompt." }),
71
- label: Type.String({ description: "The exact workflow agent label provided in the prompt." }),
72
- response: Type.Unknown({ description: "The structured response object requested by the workflow." }),
85
+ runId: Type.String({
86
+ description:
87
+ "The exact workflow response runId provided in the prompt.",
88
+ }),
89
+ label: Type.String({
90
+ description: "The exact workflow agent label provided in the prompt.",
91
+ }),
92
+ response: Type.Unknown({
93
+ description:
94
+ "The structured response object requested by the workflow.",
95
+ }),
73
96
  }),
74
97
  async execute(_toolCallId, args) {
75
98
  try {
76
- const captured = responseCollector.capture(args.runId, args.label, args.response);
99
+ const captured = responseCollector.capture(
100
+ args.runId,
101
+ args.label,
102
+ args.response,
103
+ );
77
104
  return {
78
- content: [{ type: "text", text: `Recorded workflow agent response for ${captured.label}.` }],
79
- details: { valid: true, runId: args.runId, label: captured.label, response: captured.response } as AgentResponseToolDetails,
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: `Recorded workflow agent response for ${captured.label}.`,
109
+ },
110
+ ],
111
+ details: {
112
+ valid: true,
113
+ runId: args.runId,
114
+ label: captured.label,
115
+ response: captured.response,
116
+ } as AgentResponseToolDetails,
80
117
  terminate: true,
81
118
  };
82
119
  } catch (error) {
@@ -89,7 +126,12 @@ export class NornAgentResponseToolFactory {
89
126
 
90
127
  function invalidAgentResponseToolResult(error: string) {
91
128
  return {
92
- content: [{ type: "text" as const, text: `Invalid workflow agent response: ${error}. Call the tool again with a corrected response.` }],
129
+ content: [
130
+ {
131
+ type: "text" as const,
132
+ text: `Invalid workflow agent response: ${error}. Call the tool again with a corrected response.`,
133
+ },
134
+ ],
93
135
  details: { valid: false, error } as AgentResponseToolDetails,
94
136
  };
95
137
  }
@@ -7,7 +7,16 @@ import {
7
7
  type CreateAgentSessionOptions,
8
8
  type ToolDefinition,
9
9
  } from "@earendil-works/pi-coding-agent";
10
- import type { NornAgentCreateSessionInput, NornAgentPromptInput, NornAgentRunRawAttempt, NornAgentRunResult, NornAgentSession, NornAgentSessionEvents, NornAgentSinglePromptInput, NornAgentUsage } from "@vimhead.dev/norn";
10
+ import type {
11
+ NornAgentCreateSessionInput,
12
+ NornAgentPromptInput,
13
+ NornAgentRunRawAttempt,
14
+ NornAgentRunResult,
15
+ NornAgentSession,
16
+ NornAgentSessionEvents,
17
+ NornAgentSinglePromptInput,
18
+ NornAgentUsage,
19
+ } from "@vimhead.dev/norn";
11
20
  import { AGENT_RESPONSE_TOOL_NAME } from "@vimhead.dev/norn-core/agent-protocol";
12
21
  import { inspectSchema } from "@vimhead.dev/norn/schema";
13
22
  import { randomUUID } from "node:crypto";
@@ -26,7 +35,11 @@ import { errorMessage } from "./errors.ts";
26
35
  import { safeFileName } from "./file-names.ts";
27
36
  import type { NornRunLogs } from "./logs.ts";
28
37
  import type { NornRunLogger } from "./run-log.ts";
29
- import { agentUsageFromValue, emptyAgentUsage, totalAgentUsage } from "./usage.ts";
38
+ import {
39
+ agentUsageFromValue,
40
+ emptyAgentUsage,
41
+ totalAgentUsage,
42
+ } from "./usage.ts";
30
43
 
31
44
  const DEFAULT_AGENT_ATTEMPTS = 3;
32
45
 
@@ -53,16 +66,22 @@ export class NornAgentRunner {
53
66
  private readonly responseToolFactory: NornAgentResponseToolFactory;
54
67
 
55
68
  constructor(private readonly input: NornAgentRunnerInput) {
56
- this.responseToolFactory = new NornAgentResponseToolFactory(input.responseCollector);
69
+ this.responseToolFactory = new NornAgentResponseToolFactory(
70
+ input.responseCollector,
71
+ );
57
72
  }
58
73
 
59
- async createSession(agentInput: NornAgentCreateSessionInput): Promise<NornAgentSession> {
74
+ async createSession(
75
+ agentInput: NornAgentCreateSessionInput,
76
+ ): Promise<NornAgentSession> {
60
77
  const cwd = requireAbsoluteWorkingDirectory(agentInput.cwd);
61
78
  const sessionDir = resolve(this.input.runRoot, "sessions");
62
79
  await mkdir(sessionDir, { recursive: true });
63
80
 
64
81
  const eventBus = createEventBus();
65
- const agentDir = this.input.agentDir ?? resolveNornAgentDirectory({ home: homedir(), environment: process.env });
82
+ const agentDir =
83
+ this.input.agentDir ??
84
+ resolveNornAgentDirectory({ home: homedir(), environment: process.env });
66
85
  const services = await createAgentSessionServices({
67
86
  cwd,
68
87
  agentDir,
@@ -74,15 +93,33 @@ export class NornAgentRunner {
74
93
  });
75
94
  const loader = services.resourceLoader;
76
95
  const errors = [
77
- ...services.diagnostics.filter(diagnostic => diagnostic.type === "error").map(diagnostic => diagnostic.message),
78
- ...loader.getExtensions().errors.map(error => `Failed to load extension ${error.path}: ${error.error}`),
96
+ ...services.diagnostics
97
+ .filter((diagnostic) => diagnostic.type === "error")
98
+ .map((diagnostic) => diagnostic.message),
99
+ ...loader
100
+ .getExtensions()
101
+ .errors.map(
102
+ (error) => `Failed to load extension ${error.path}: ${error.error}`,
103
+ ),
79
104
  ];
80
105
  if (errors.length > 0) throw new Error(errors.join("\n"));
81
106
  const customTools = agentInput.customTools ?? [];
82
107
  assertCustomToolNames({
83
108
  tools: customTools,
84
- reservedNames: ["read", "bash", "edit", "write", "grep", "find", "ls", "powershell", AGENT_RESPONSE_TOOL_NAME,
85
- ...loader.getExtensions().extensions.flatMap((extension) => [...extension.tools.keys()])],
109
+ reservedNames: [
110
+ "read",
111
+ "bash",
112
+ "edit",
113
+ "write",
114
+ "grep",
115
+ "find",
116
+ "ls",
117
+ "powershell",
118
+ AGENT_RESPONSE_TOOL_NAME,
119
+ ...loader
120
+ .getExtensions()
121
+ .extensions.flatMap((extension) => [...extension.tools.keys()]),
122
+ ],
86
123
  });
87
124
  let session: AgentSession | undefined;
88
125
  try {
@@ -96,19 +133,34 @@ export class NornAgentRunner {
96
133
  }));
97
134
  await agentInput.beforeSessionStart?.({ events: eventBus });
98
135
  await session.bindExtensions({});
99
- await this.input.logger.record({ type: "agent.spawned", label: agentInput.label, cwd });
136
+ await this.input.logger.record({
137
+ type: "agent.spawned",
138
+ label: agentInput.label,
139
+ cwd,
140
+ });
100
141
  return new CreatedNornAgentSession({
101
- ...this.input, label: agentInput.label, cwd, session, events: eventBus,
142
+ ...this.input,
143
+ label: agentInput.label,
144
+ cwd,
145
+ session,
146
+ events: eventBus,
102
147
  });
103
148
  } catch (error) {
104
149
  const errors = [error];
105
- try { session?.dispose(); } catch (cleanupError) { errors.push(cleanupError); }
106
- if (errors.length > 1) throw new AggregateError(errors, "Agent creation and cleanup failed");
150
+ try {
151
+ session?.dispose();
152
+ } catch (cleanupError) {
153
+ errors.push(cleanupError);
154
+ }
155
+ if (errors.length > 1)
156
+ throw new AggregateError(errors, "Agent creation and cleanup failed");
107
157
  throw error;
108
158
  }
109
159
  }
110
160
 
111
- async prompt<ResponseSchema extends TSchema>(agentInput: NornAgentSinglePromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>> {
161
+ async prompt<ResponseSchema extends TSchema>(
162
+ agentInput: NornAgentSinglePromptInput<ResponseSchema>,
163
+ ): Promise<StaticDecode<ResponseSchema>> {
112
164
  const agent = await this.createSession(agentInput);
113
165
  try {
114
166
  return await agent.prompt(agentInput);
@@ -118,14 +170,20 @@ export class NornAgentRunner {
118
170
  }
119
171
  }
120
172
 
121
- function systemPromptOverride(agentInput: NornAgentCreateSessionInput): (() => string) | undefined {
173
+ function systemPromptOverride(
174
+ agentInput: NornAgentCreateSessionInput,
175
+ ): (() => string) | undefined {
122
176
  const systemPrompt = agentInput.systemPrompt;
123
177
  return systemPrompt === undefined ? undefined : () => systemPrompt;
124
178
  }
125
179
 
126
- function appendSystemPromptOverride(agentInput: NornAgentCreateSessionInput): ((base: string[]) => string[]) | undefined {
180
+ function appendSystemPromptOverride(
181
+ agentInput: NornAgentCreateSessionInput,
182
+ ): ((base: string[]) => string[]) | undefined {
127
183
  const appendSystemPrompt = agentInput.appendSystemPrompt;
128
- return appendSystemPrompt === undefined ? undefined : (base) => [...base, ...appendSystemPrompt];
184
+ return appendSystemPrompt === undefined
185
+ ? undefined
186
+ : (base) => [...base, ...appendSystemPrompt];
129
187
  }
130
188
 
131
189
  class CreatedNornAgentSession implements NornAgentSession {
@@ -140,29 +198,53 @@ class CreatedNornAgentSession implements NornAgentSession {
140
198
  this.events = input.events;
141
199
  }
142
200
 
143
- async prompt<ResponseSchema extends TSchema>(agentInput: NornAgentPromptInput<ResponseSchema>): Promise<StaticDecode<ResponseSchema>> {
201
+ async prompt<ResponseSchema extends TSchema>(
202
+ agentInput: NornAgentPromptInput<ResponseSchema>,
203
+ ): Promise<StaticDecode<ResponseSchema>> {
144
204
  return (await this.promptWithResult(agentInput)).response;
145
205
  }
146
206
 
147
- private async promptWithResult<ResponseSchema extends TSchema>(agentInput: NornAgentPromptInput<ResponseSchema>): Promise<NornAgentRunResult<ResponseSchema>> {
148
- if (this.isDisposed) throw new Error(`Workflow agent session is disposed: ${this.label}`);
149
- const maxAttempts = Math.max(1, Math.floor(agentInput.maxAttempts ?? DEFAULT_AGENT_ATTEMPTS));
207
+ private async promptWithResult<ResponseSchema extends TSchema>(
208
+ agentInput: NornAgentPromptInput<ResponseSchema>,
209
+ ): Promise<NornAgentRunResult<ResponseSchema>> {
210
+ if (this.isDisposed)
211
+ throw new Error(`Workflow agent session is disposed: ${this.label}`);
212
+ const maxAttempts = Math.max(
213
+ 1,
214
+ Math.floor(agentInput.maxAttempts ?? DEFAULT_AGENT_ATTEMPTS),
215
+ );
150
216
  const attempts: NornAgentRunRawAttempt[] = [];
151
217
  const startedAtMs = Date.now();
152
218
  let isTerminalEventRecorded = false;
153
- await this.input.logger.record({ type: "agent.started", label: this.label, cwd: this.cwd, maxAttempts });
219
+ await this.input.logger.record({
220
+ type: "agent.started",
221
+ label: this.label,
222
+ cwd: this.cwd,
223
+ maxAttempts,
224
+ });
154
225
 
155
226
  try {
156
227
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
157
- await this.input.logger.record({ type: "agent.attempt.started", label: this.label, attempt });
158
- const raw = await this.runAttempt(agentInput, attempt, attempts.at(-1)?.error);
228
+ await this.input.logger.record({
229
+ type: "agent.attempt.started",
230
+ label: this.label,
231
+ attempt,
232
+ });
233
+ const raw = await this.runAttempt(
234
+ agentInput,
235
+ attempt,
236
+ attempts.at(-1)?.error,
237
+ );
159
238
  const rawAttempt: NornAgentRunRawAttempt = { attempt, ...raw };
160
239
  attempts.push(rawAttempt);
161
240
 
162
241
  try {
163
- if (!raw.responseToolCalled) throw new Error(`Agent did not call ${AGENT_RESPONSE_TOOL_NAME}`);
242
+ if (!raw.responseToolCalled)
243
+ throw new Error(`Agent did not call ${AGENT_RESPONSE_TOOL_NAME}`);
164
244
  const response = raw.toolResponse as StaticDecode<ResponseSchema>;
165
- const usage = totalAgentUsage(attempts.map((candidate) => candidate.usage));
245
+ const usage = totalAgentUsage(
246
+ attempts.map((candidate) => candidate.usage),
247
+ );
166
248
  const result: NornAgentRunResult<ResponseSchema> = {
167
249
  label: this.label,
168
250
  cwd: this.cwd,
@@ -170,22 +252,49 @@ class CreatedNornAgentSession implements NornAgentSession {
170
252
  usage,
171
253
  raw: { ...raw, usage, attempts },
172
254
  };
173
- await this.input.logs.write(`agents/${safeFileName(this.label)}.json`, JSON.stringify(result, null, 2));
255
+ await this.input.logs.write(
256
+ `agents/${safeFileName(this.label)}.json`,
257
+ JSON.stringify(result, null, 2),
258
+ );
174
259
  isTerminalEventRecorded = true;
175
- await this.input.logger.record({ type: "agent.completed", label: this.label, attempts: attempt, durationMs: Date.now() - startedAtMs, usage });
260
+ await this.input.logger.record({
261
+ type: "agent.completed",
262
+ label: this.label,
263
+ attempts: attempt,
264
+ durationMs: Date.now() - startedAtMs,
265
+ usage,
266
+ });
176
267
  return result;
177
268
  } catch (error) {
178
269
  const message = errorMessage(error);
179
270
  attempts[attempts.length - 1] = { ...rawAttempt, error: message };
180
271
  await this.input.logs.write(
181
272
  `agents/${safeFileName(this.label)}.attempt-${attempt}.raw.json`,
182
- JSON.stringify({ label: this.label, cwd: this.cwd, raw: attempts.at(-1) }, null, 2),
273
+ JSON.stringify(
274
+ { label: this.label, cwd: this.cwd, raw: attempts.at(-1) },
275
+ null,
276
+ 2,
277
+ ),
183
278
  );
184
- await this.input.logger.record({ type: "agent.attempt.failed", label: this.label, attempt, error: message });
279
+ await this.input.logger.record({
280
+ type: "agent.attempt.failed",
281
+ label: this.label,
282
+ attempt,
283
+ error: message,
284
+ });
185
285
  if (attempt === maxAttempts) {
186
- const usage = totalAgentUsage(attempts.map((candidate) => candidate.usage));
286
+ const usage = totalAgentUsage(
287
+ attempts.map((candidate) => candidate.usage),
288
+ );
187
289
  isTerminalEventRecorded = true;
188
- await this.input.logger.record({ type: "agent.failed", label: this.label, attempts: attempt, durationMs: Date.now() - startedAtMs, usage, error: message });
290
+ await this.input.logger.record({
291
+ type: "agent.failed",
292
+ label: this.label,
293
+ attempts: attempt,
294
+ durationMs: Date.now() - startedAtMs,
295
+ usage,
296
+ error: message,
297
+ });
189
298
  throw new Error(
190
299
  `Agent ${this.label} did not return a valid structured response after ${attempt} attempt(s). Raw output saved to logs/agents/${safeFileName(this.label)}.attempt-${attempt}.raw.json: ${message}`,
191
300
  );
@@ -213,10 +322,22 @@ class CreatedNornAgentSession implements NornAgentSession {
213
322
  if (this.isDisposed) return;
214
323
  this.isDisposed = true;
215
324
  const errors: unknown[] = [];
216
- try { await this.input.session.abort(); } catch (error) { errors.push(error); }
217
- try { this.input.session.dispose(); } catch (error) { errors.push(error); }
218
- if (errors.length) throw new AggregateError(errors, "Agent disposal failed");
219
- await this.input.logger.record({ type: "agent.disposed", label: this.label });
325
+ try {
326
+ await this.input.session.abort();
327
+ } catch (error) {
328
+ errors.push(error);
329
+ }
330
+ try {
331
+ this.input.session.dispose();
332
+ } catch (error) {
333
+ errors.push(error);
334
+ }
335
+ if (errors.length)
336
+ throw new AggregateError(errors, "Agent disposal failed");
337
+ await this.input.logger.record({
338
+ type: "agent.disposed",
339
+ label: this.label,
340
+ });
220
341
  }
221
342
 
222
343
  private async runAttempt<ResponseSchema extends TSchema>(
@@ -227,29 +348,54 @@ class CreatedNornAgentSession implements NornAgentSession {
227
348
  const responseRunId = `${this.input.id}:${attempt}:${randomUUID()}`;
228
349
  const messages: unknown[] = [];
229
350
  let text = "";
230
- const releaseResponseSlot = this.input.responseCollector.begin(responseRunId, this.label, agentInput.response);
351
+ const releaseResponseSlot = this.input.responseCollector.begin(
352
+ responseRunId,
353
+ this.label,
354
+ agentInput.response,
355
+ );
231
356
  const unsubscribe = this.input.session.subscribe((event) => {
232
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
357
+ if (
358
+ event.type === "message_update" &&
359
+ event.assistantMessageEvent.type === "text_delta"
360
+ ) {
233
361
  text += event.assistantMessageEvent.delta;
234
362
  }
235
- if (event.type === "message_end" && event.message) messages.push(event.message);
363
+ if (event.type === "message_end" && event.message)
364
+ messages.push(event.message);
236
365
  });
237
366
  const abortNestedSession = () => {
238
367
  void this.input.session.abort();
239
368
  };
240
- this.input.signal?.addEventListener("abort", abortNestedSession, { once: true });
369
+ this.input.signal?.addEventListener("abort", abortNestedSession, {
370
+ once: true,
371
+ });
241
372
 
242
373
  let capturedResponse: NornCapturedAgentResponse = { called: false };
243
374
  try {
244
375
  this.input.signal?.throwIfAborted();
245
- await this.input.session.prompt(withResponseToolInstruction(agentInput.prompt, agentInput.response, this.label, responseRunId, attempt, previousError), {
246
- ...agentInput.options,
247
- source: agentInput.options?.source ?? "extension",
248
- });
376
+ await this.input.session.prompt(
377
+ withResponseToolInstruction(
378
+ agentInput.prompt,
379
+ agentInput.response,
380
+ this.label,
381
+ responseRunId,
382
+ attempt,
383
+ previousError,
384
+ ),
385
+ {
386
+ ...agentInput.options,
387
+ source: agentInput.options?.source ?? "extension",
388
+ },
389
+ );
249
390
  this.input.signal?.throwIfAborted();
250
391
  capturedResponse = this.input.responseCollector.get(responseRunId);
251
392
  if (!capturedResponse.called) {
252
- await this.promptForStructuredResponse(agentInput, responseRunId, attempt, previousError);
393
+ await this.promptForStructuredResponse(
394
+ agentInput,
395
+ responseRunId,
396
+ attempt,
397
+ previousError,
398
+ );
253
399
  this.input.signal?.throwIfAborted();
254
400
  capturedResponse = this.input.responseCollector.get(responseRunId);
255
401
  }
@@ -276,16 +422,38 @@ class CreatedNornAgentSession implements NornAgentSession {
276
422
  previousError: string | undefined,
277
423
  ): Promise<void> {
278
424
  const activeToolNames = this.input.session.getActiveToolNames();
279
- await this.input.logger.record({ type: "agent.response-finalization.started", label: this.label, attempt });
425
+ await this.input.logger.record({
426
+ type: "agent.response-finalization.started",
427
+ label: this.label,
428
+ attempt,
429
+ });
280
430
  this.input.session.setActiveToolsByName([AGENT_RESPONSE_TOOL_NAME]);
281
431
  try {
282
- await this.input.session.prompt(withResponseToolFinalizationInstruction(agentInput.response, this.label, responseRunId, attempt, previousError), {
283
- ...agentInput.options,
284
- source: agentInput.options?.source ?? "extension",
432
+ await this.input.session.prompt(
433
+ withResponseToolFinalizationInstruction(
434
+ agentInput.response,
435
+ this.label,
436
+ responseRunId,
437
+ attempt,
438
+ previousError,
439
+ ),
440
+ {
441
+ ...agentInput.options,
442
+ source: agentInput.options?.source ?? "extension",
443
+ },
444
+ );
445
+ await this.input.logger.record({
446
+ type: "agent.response-finalization.completed",
447
+ label: this.label,
448
+ attempt,
285
449
  });
286
- await this.input.logger.record({ type: "agent.response-finalization.completed", label: this.label, attempt });
287
450
  } catch (error) {
288
- await this.input.logger.record({ type: "agent.response-finalization.failed", label: this.label, attempt, error: errorMessage(error) });
451
+ await this.input.logger.record({
452
+ type: "agent.response-finalization.failed",
453
+ label: this.label,
454
+ attempt,
455
+ error: errorMessage(error),
456
+ });
289
457
  throw error;
290
458
  } finally {
291
459
  this.input.session.setActiveToolsByName(activeToolNames);
@@ -294,12 +462,20 @@ class CreatedNornAgentSession implements NornAgentSession {
294
462
  }
295
463
 
296
464
  function usageFromMessages(messages: readonly unknown[]): NornAgentUsage {
297
- return totalAgentUsage(messages.map(usageFromMessage).filter((usage): usage is NornAgentUsage => usage !== undefined));
465
+ return totalAgentUsage(
466
+ messages
467
+ .map(usageFromMessage)
468
+ .filter((usage): usage is NornAgentUsage => usage !== undefined),
469
+ );
298
470
  }
299
471
 
300
472
  function usageFromMessage(message: unknown): NornAgentUsage | undefined {
301
- if (!message || typeof message !== "object" || !("usage" in message)) return undefined;
302
- return agentUsageFromValue((message as { usage?: unknown }).usage) ?? emptyAgentUsage();
473
+ if (!message || typeof message !== "object" || !("usage" in message))
474
+ return undefined;
475
+ return (
476
+ agentUsageFromValue((message as { usage?: unknown }).usage) ??
477
+ emptyAgentUsage()
478
+ );
303
479
  }
304
480
 
305
481
  function withResponseToolInstruction(
@@ -321,7 +497,9 @@ function withResponseToolInstruction(
321
497
  "Pass the structured workflow response in the tool's response argument.",
322
498
  "The response argument must match this JSON Schema:",
323
499
  JSON.stringify(inspectSchema(responseSchema), null, 2),
324
- ...(attempt > 1 && previousError ? ["", `Previous structured response attempt failed: ${previousError}`] : []),
500
+ ...(attempt > 1 && previousError
501
+ ? ["", `Previous structured response attempt failed: ${previousError}`]
502
+ : []),
325
503
  ].join("\n");
326
504
  }
327
505
 
@@ -341,18 +519,28 @@ function withResponseToolFinalizationInstruction(
341
519
  "Pass the structured workflow response in the tool's response argument, based on the work you already completed in this session.",
342
520
  "The response argument must match this JSON Schema:",
343
521
  JSON.stringify(inspectSchema(responseSchema), null, 2),
344
- ...(attempt > 1 && previousError ? ["", `Previous structured response attempt failed: ${previousError}`] : []),
522
+ ...(attempt > 1 && previousError
523
+ ? ["", `Previous structured response attempt failed: ${previousError}`]
524
+ : []),
345
525
  ].join("\n");
346
526
  }
347
527
 
348
- function withAgentResponseTool(tools: readonly string[] | undefined): string[] | undefined {
349
- return tools === undefined ? undefined : Array.from(new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
528
+ function withAgentResponseTool(
529
+ tools: readonly string[] | undefined,
530
+ ): string[] | undefined {
531
+ return tools === undefined
532
+ ? undefined
533
+ : Array.from(new Set([...tools, AGENT_RESPONSE_TOOL_NAME]));
350
534
  }
351
535
 
352
- function assertCustomToolNames(input: { readonly tools: readonly ToolDefinition[]; readonly reservedNames: readonly string[] }): void {
536
+ function assertCustomToolNames(input: {
537
+ readonly tools: readonly ToolDefinition[];
538
+ readonly reservedNames: readonly string[];
539
+ }): void {
353
540
  const names = new Set(input.reservedNames);
354
541
  for (const tool of input.tools) {
355
- if (names.has(tool.name)) throw new Error(`Custom tool name collision: ${tool.name}`);
542
+ if (names.has(tool.name))
543
+ throw new Error(`Custom tool name collision: ${tool.name}`);
356
544
  names.add(tool.name);
357
545
  }
358
546
  }