@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.
- package/assets/README.md +41 -13
- package/assets/docs/README.md +16 -16
- package/assets/docs/agents.md +23 -15
- package/assets/docs/cli.md +10 -8
- package/assets/docs/composition.md +7 -7
- package/assets/docs/persistence.md +22 -22
- package/assets/docs/projects.md +6 -6
- package/assets/docs/recovery.md +10 -10
- package/assets/docs/schemas.md +42 -36
- package/assets/docs/workflows.md +39 -32
- package/assets/examples/agent-then-analysis/input.json +3 -3
- package/assets/examples/agent-then-analysis/norn.project.json +2 -2
- package/assets/examples/agent-then-analysis/plugin.ts +37 -9
- package/assets/examples/caller-owned-routing/README.md +6 -6
- package/assets/examples/caller-owned-routing/assessment.ts +18 -4
- package/assets/examples/caller-owned-routing/contracts.ts +7 -2
- package/assets/examples/caller-owned-routing/input.json +12 -12
- package/assets/examples/caller-owned-routing/norn.project.json +2 -2
- package/assets/examples/caller-owned-routing/revision.ts +7 -2
- package/assets/examples/caller-owned-routing/router.ts +14 -3
- package/assets/examples/caller-selected-continuation/caller.ts +38 -12
- package/assets/examples/caller-selected-continuation/input.json +7 -7
- package/assets/examples/caller-selected-continuation/norn.project.json +2 -2
- package/assets/examples/caller-selected-continuation/producer.ts +10 -3
- package/assets/examples/coordinating-multiple-agents/README.md +6 -6
- package/assets/examples/coordinating-multiple-agents/input.json +20 -8
- package/assets/examples/coordinating-multiple-agents/norn.project.json +2 -2
- package/assets/examples/coordinating-multiple-agents/plugin.ts +165 -46
- package/assets/examples/coordinating-multiple-agents/queue-tools.ts +30 -7
- package/assets/examples/coordinating-multiple-agents/work-queue.ts +211 -63
- package/assets/examples/getting-started/norn.project.json +2 -2
- package/assets/examples/getting-started/plugin.ts +33 -13
- package/assets/examples/minimal-workflow/norn.project.json +2 -2
- package/assets/examples/minimal-workflow/plugin.ts +12 -3
- package/assets/examples/shared-state/README.md +6 -6
- package/assets/examples/shared-state/input.json +1 -1
- package/assets/examples/shared-state/norn.project.json +2 -2
- package/assets/examples/shared-state/plugin.ts +44 -15
- package/assets/examples/shared-state/shared-state.ts +46 -13
- package/assets/examples/shared-state/state-tools.ts +68 -16
- package/assets/examples/worktree-development-loop/README.md +12 -12
- package/assets/examples/worktree-development-loop/norn.project.json +7 -7
- package/assets/examples/worktree-development-loop/plugin.ts +7 -1
- package/assets/examples/worktree-development-loop/scope.ts +4 -1
- package/assets/examples/worktree-development-loop/shared/commands.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/development-loop/execute.ts +18 -4
- package/assets/examples/worktree-development-loop/workflows/development-loop/index.ts +4 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/repository.ts +11 -1
- package/assets/examples/worktree-development-loop/workflows/development-loop/schema.ts +6 -2
- package/assets/examples/worktree-development-loop/workflows/implementation/execute.ts +40 -10
- package/assets/examples/worktree-development-loop/workflows/implementation/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/planning/execute.ts +1 -1
- package/assets/examples/worktree-development-loop/workflows/planning/index.ts +5 -1
- package/assets/examples/worktree-development-loop/workflows/review/execute.ts +33 -9
- package/assets/examples/worktree-development-loop/workflows/review-router/execute.ts +51 -8
- package/assets/package.json +1 -1
- package/assets/packages/cli/src/build-info.ts +4 -1
- package/assets/packages/cli/src/bun/cli.ts +21 -5
- package/assets/packages/cli/src/cli.ts +585 -170
- package/assets/packages/cli/src/client.ts +194 -36
- package/assets/packages/cli/src/documentation.ts +135 -30
- package/assets/packages/cli/src/generated-build-info.ts +2 -2
- package/assets/packages/cli/src/internal/agent-directory.ts +7 -2
- package/assets/packages/cli/src/internal/agent-response-tool.ts +57 -15
- package/assets/packages/cli/src/internal/agents.ts +248 -60
- package/assets/packages/cli/src/internal/commands.ts +82 -20
- package/assets/packages/cli/src/internal/documentation-bundle.ts +44 -13
- package/assets/packages/cli/src/internal/engine.ts +396 -87
- package/assets/packages/cli/src/internal/errors.ts +13 -5
- package/assets/packages/cli/src/internal/execution-context.ts +20 -12
- package/assets/packages/cli/src/internal/file-coordinator.ts +95 -26
- package/assets/packages/cli/src/internal/file-names.ts +4 -1
- package/assets/packages/cli/src/internal/launch-request.ts +74 -22
- package/assets/packages/cli/src/internal/logs.ts +10 -2
- package/assets/packages/cli/src/internal/metrics.ts +247 -61
- package/assets/packages/cli/src/internal/pi-assets.ts +105 -23
- package/assets/packages/cli/src/internal/run-lease.ts +72 -22
- package/assets/packages/cli/src/internal/run-log.ts +49 -20
- package/assets/packages/cli/src/internal/run-names.ts +120 -12
- package/assets/packages/cli/src/internal/run-state.ts +306 -71
- package/assets/packages/cli/src/internal/run-store.ts +216 -53
- package/assets/packages/cli/src/internal/usage.ts +23 -7
- package/assets/packages/cli/src/internal/worker-directory.ts +9 -3
- package/assets/packages/cli/src/internal/workflow-registry.ts +281 -70
- package/assets/packages/cli/src/internal/working-directory.ts +2 -1
- package/assets/packages/cli/src/workflow-loader.ts +341 -74
- package/assets/packages/core/src/atomic-files.ts +13 -4
- package/assets/packages/core/src/workflow-transition.ts +13 -2
- package/assets/packages/sdk/src/api.ts +248 -80
- package/assets/packages/sdk/src/index.ts +6 -1
- package/assets/packages/sdk/src/schema.ts +75 -21
- package/assets/tests/workflow-ref.test.ts +242 -57
- package/bin/norn.mjs +13 -4
- package/dist/cli.js +344 -90
- package/dist/client.js +100 -22
- package/dist/documentation.js +102 -25
- package/dist/generated-build-info.d.ts +2 -2
- package/dist/generated-build-info.js +2 -2
- package/dist/internal/agent-directory.js +3 -1
- package/dist/internal/agent-response-tool.js +42 -11
- package/dist/internal/agents.js +149 -37
- package/dist/internal/commands.js +65 -16
- package/dist/internal/documentation-bundle.js +23 -11
- package/dist/internal/engine.js +304 -66
- package/dist/internal/errors.js +4 -2
- package/dist/internal/execution-context.js +2 -1
- package/dist/internal/file-coordinator.js +59 -19
- package/dist/internal/launch-request.js +32 -14
- package/dist/internal/metrics.js +138 -38
- package/dist/internal/pi-assets.js +70 -19
- package/dist/internal/run-lease.js +29 -9
- package/dist/internal/run-log.js +24 -12
- package/dist/internal/run-state.js +172 -48
- package/dist/internal/run-store.js +157 -45
- package/dist/internal/worker-directory.js +6 -2
- package/dist/internal/workflow-registry.js +123 -32
- package/dist/internal/working-directory.js +2 -1
- package/dist/workflow-loader.js +198 -41
- package/package.json +2 -2
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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)
|
|
45
|
+
if (!pending)
|
|
46
|
+
throw new Error(`No active workflow agent response slot: ${runId}`);
|
|
39
47
|
if (pending.label !== label) {
|
|
40
|
-
throw new Error(
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Workflow agent response label mismatch: expected ${pending.label}, received ${label}`,
|
|
50
|
+
);
|
|
41
51
|
}
|
|
42
|
-
if (pending.called)
|
|
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
|
|
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:
|
|
64
|
-
|
|
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({
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
99
|
+
const captured = responseCollector.capture(
|
|
100
|
+
args.runId,
|
|
101
|
+
args.label,
|
|
102
|
+
args.response,
|
|
103
|
+
);
|
|
77
104
|
return {
|
|
78
|
-
content: [
|
|
79
|
-
|
|
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: [
|
|
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 {
|
|
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 {
|
|
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(
|
|
69
|
+
this.responseToolFactory = new NornAgentResponseToolFactory(
|
|
70
|
+
input.responseCollector,
|
|
71
|
+
);
|
|
57
72
|
}
|
|
58
73
|
|
|
59
|
-
async createSession(
|
|
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 =
|
|
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
|
|
78
|
-
|
|
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: [
|
|
85
|
-
|
|
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({
|
|
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,
|
|
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 {
|
|
106
|
-
|
|
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>(
|
|
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(
|
|
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(
|
|
180
|
+
function appendSystemPromptOverride(
|
|
181
|
+
agentInput: NornAgentCreateSessionInput,
|
|
182
|
+
): ((base: string[]) => string[]) | undefined {
|
|
127
183
|
const appendSystemPrompt = agentInput.appendSystemPrompt;
|
|
128
|
-
return appendSystemPrompt === undefined
|
|
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>(
|
|
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>(
|
|
148
|
-
|
|
149
|
-
|
|
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({
|
|
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({
|
|
158
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
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({
|
|
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(
|
|
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({
|
|
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(
|
|
286
|
+
const usage = totalAgentUsage(
|
|
287
|
+
attempts.map((candidate) => candidate.usage),
|
|
288
|
+
);
|
|
187
289
|
isTerminalEventRecorded = true;
|
|
188
|
-
await this.input.logger.record({
|
|
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 {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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(
|
|
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 (
|
|
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)
|
|
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, {
|
|
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(
|
|
246
|
-
|
|
247
|
-
|
|
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(
|
|
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({
|
|
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(
|
|
283
|
-
|
|
284
|
-
|
|
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({
|
|
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(
|
|
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))
|
|
302
|
-
|
|
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
|
|
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
|
|
522
|
+
...(attempt > 1 && previousError
|
|
523
|
+
? ["", `Previous structured response attempt failed: ${previousError}`]
|
|
524
|
+
: []),
|
|
345
525
|
].join("\n");
|
|
346
526
|
}
|
|
347
527
|
|
|
348
|
-
function withAgentResponseTool(
|
|
349
|
-
|
|
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: {
|
|
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))
|
|
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
|
}
|