nolo-cli 0.1.20 → 0.1.21
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/ai/taskRun/taskRunProtocol.ts +59 -0
- package/client/agentRun.test.ts +69 -0
- package/client/agentRun.ts +57 -3
- package/package.json +1 -1
- package/taskRunCommand.ts +28 -0
|
@@ -10,6 +10,7 @@ export const TASK_RUN_TOOL_ACTIONS = [
|
|
|
10
10
|
"upsertWorkItem",
|
|
11
11
|
"claimOrDispatch",
|
|
12
12
|
"claimWorkItem",
|
|
13
|
+
"recordAgentRun",
|
|
13
14
|
"openDialog",
|
|
14
15
|
"recordArtifact",
|
|
15
16
|
"markBlocked",
|
|
@@ -79,6 +80,13 @@ export type TaskRunAgentRun = {
|
|
|
79
80
|
updatedAt: string;
|
|
80
81
|
resultSummary?: string;
|
|
81
82
|
errorCode?: string;
|
|
83
|
+
toolsUsed?: string[];
|
|
84
|
+
toolTraceSummary?: {
|
|
85
|
+
callCount: number;
|
|
86
|
+
resultCount: number;
|
|
87
|
+
errorCount: number;
|
|
88
|
+
rounds: number;
|
|
89
|
+
};
|
|
82
90
|
};
|
|
83
91
|
|
|
84
92
|
export type TaskRunArtifact = {
|
|
@@ -614,6 +622,57 @@ export function recordTaskRunAgentDispatch(args: {
|
|
|
614
622
|
return { meta: updated.meta, workItem: updated.workItem, agentRun };
|
|
615
623
|
}
|
|
616
624
|
|
|
625
|
+
export function recordTaskRunAgentRun(args: {
|
|
626
|
+
meta: TaskRunMeta;
|
|
627
|
+
run: Omit<TaskRunAgentRun, "id" | "startedAt" | "updatedAt"> & {
|
|
628
|
+
id?: string;
|
|
629
|
+
startedAt?: string;
|
|
630
|
+
};
|
|
631
|
+
now?: string;
|
|
632
|
+
createId?: () => string;
|
|
633
|
+
}): { meta: TaskRunMeta; agentRun: TaskRunAgentRun } {
|
|
634
|
+
const now = args.now ?? new Date().toISOString();
|
|
635
|
+
const meta = cloneMeta(args.meta);
|
|
636
|
+
const agentRuns = [...(meta.agentRuns ?? [])];
|
|
637
|
+
const id = args.run.id ?? args.createId?.() ?? createProtocolId();
|
|
638
|
+
const index = agentRuns.findIndex((run) => run.id === id);
|
|
639
|
+
const existing = index >= 0 ? agentRuns[index] : undefined;
|
|
640
|
+
const agentRun: TaskRunAgentRun = {
|
|
641
|
+
...(existing ?? {
|
|
642
|
+
id,
|
|
643
|
+
agentKey: args.run.agentKey,
|
|
644
|
+
role: args.run.role,
|
|
645
|
+
startedAt: args.run.startedAt ?? now,
|
|
646
|
+
}),
|
|
647
|
+
...args.run,
|
|
648
|
+
id,
|
|
649
|
+
startedAt: existing?.startedAt ?? args.run.startedAt ?? now,
|
|
650
|
+
updatedAt: now,
|
|
651
|
+
};
|
|
652
|
+
if (index >= 0) {
|
|
653
|
+
agentRuns[index] = agentRun;
|
|
654
|
+
} else {
|
|
655
|
+
agentRuns.push(agentRun);
|
|
656
|
+
}
|
|
657
|
+
meta.agentRuns = agentRuns;
|
|
658
|
+
appendTaskRunEvent({
|
|
659
|
+
meta,
|
|
660
|
+
type: "agent_run.recorded",
|
|
661
|
+
agentKey: agentRun.agentKey,
|
|
662
|
+
workItemId: agentRun.workItemId,
|
|
663
|
+
dialogId: agentRun.dialogId,
|
|
664
|
+
now,
|
|
665
|
+
payload: {
|
|
666
|
+
agentRunId: agentRun.id,
|
|
667
|
+
status: agentRun.status,
|
|
668
|
+
toolsUsed: agentRun.toolsUsed,
|
|
669
|
+
toolTraceSummary: agentRun.toolTraceSummary,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
touchTaskRun(meta, now, agentRun.status === "running" ? "running" : undefined);
|
|
673
|
+
return { meta, agentRun };
|
|
674
|
+
}
|
|
675
|
+
|
|
617
676
|
export function submitTaskRunOutcome(args: {
|
|
618
677
|
meta: TaskRunMeta;
|
|
619
678
|
workItemId: string;
|
package/client/agentRun.test.ts
CHANGED
|
@@ -452,6 +452,75 @@ describe("cli agent run client", () => {
|
|
|
452
452
|
expect(output.text()).toContain("[nolo:tool] round 1 <- gitStatus (call-status)");
|
|
453
453
|
});
|
|
454
454
|
|
|
455
|
+
test("passes local tool evidence to task-run writeback", async () => {
|
|
456
|
+
const output = new CaptureOutput();
|
|
457
|
+
const writebacks: any[] = [];
|
|
458
|
+
let completeCalls = 0;
|
|
459
|
+
|
|
460
|
+
const result = await runAgentTurn({
|
|
461
|
+
agentName: "frontend",
|
|
462
|
+
agentKey: "frontend-local",
|
|
463
|
+
serverUrl: "https://nolo.chat",
|
|
464
|
+
message: "inspect repo",
|
|
465
|
+
scriptDir: "C:/missing/scripts",
|
|
466
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
467
|
+
output,
|
|
468
|
+
runtimeMode: "local",
|
|
469
|
+
taskRunContext: {
|
|
470
|
+
rowDbKey: "row-b2e06f801f-01TASK",
|
|
471
|
+
workItemId: "self",
|
|
472
|
+
},
|
|
473
|
+
taskRunRecorder: async (args) => {
|
|
474
|
+
writebacks.push(args);
|
|
475
|
+
},
|
|
476
|
+
localRuntimeAdapter: {
|
|
477
|
+
host: "cli",
|
|
478
|
+
capabilities: ["local-provider", "local-persistence", "local-tools"],
|
|
479
|
+
loadAgentConfig: async (agentRef) => ({
|
|
480
|
+
key: agentRef,
|
|
481
|
+
name: "Frontend",
|
|
482
|
+
prompt: "Fix UI",
|
|
483
|
+
model: "fake-local",
|
|
484
|
+
toolNames: ["gitStatus"],
|
|
485
|
+
}),
|
|
486
|
+
loadDialogHistory: async () => [],
|
|
487
|
+
saveTurn: async () => ({ dialogId: "dialog-local" }),
|
|
488
|
+
resolveProvider: async () => ({
|
|
489
|
+
model: "fake-local",
|
|
490
|
+
complete: async () => {
|
|
491
|
+
completeCalls += 1;
|
|
492
|
+
if (completeCalls === 1) {
|
|
493
|
+
return {
|
|
494
|
+
content: "",
|
|
495
|
+
model: "fake-local",
|
|
496
|
+
tool_calls: [{
|
|
497
|
+
id: "call-status",
|
|
498
|
+
type: "function",
|
|
499
|
+
function: { name: "gitStatus", arguments: "{}" },
|
|
500
|
+
}],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return { content: "done", model: "fake-local" };
|
|
504
|
+
},
|
|
505
|
+
}),
|
|
506
|
+
executeTool: async () => ({ content: "clean" }),
|
|
507
|
+
},
|
|
508
|
+
scriptPathExists: () => false,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
|
|
512
|
+
expect(writebacks).toHaveLength(1);
|
|
513
|
+
expect(writebacks[0].toolEvidence).toEqual({
|
|
514
|
+
toolsUsed: ["gitStatus"],
|
|
515
|
+
summary: {
|
|
516
|
+
callCount: 1,
|
|
517
|
+
resultCount: 1,
|
|
518
|
+
errorCount: 0,
|
|
519
|
+
rounds: 1,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
455
524
|
test("auto mode prefers a working local runtime before HTTP", async () => {
|
|
456
525
|
const output = new CaptureOutput();
|
|
457
526
|
const httpCalls: string[] = [];
|
package/client/agentRun.ts
CHANGED
|
@@ -15,6 +15,15 @@ type OutputLike = {
|
|
|
15
15
|
|
|
16
16
|
type TaskRunAgentRole = "pm" | "frontend" | "fullstack" | "reviewer" | "codex";
|
|
17
17
|
type TaskRunReviewStatus = "passed" | "needs_changes" | "blocked";
|
|
18
|
+
type TaskRunToolEvidence = {
|
|
19
|
+
toolsUsed: string[];
|
|
20
|
+
summary: {
|
|
21
|
+
callCount: number;
|
|
22
|
+
resultCount: number;
|
|
23
|
+
errorCount: number;
|
|
24
|
+
rounds: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
18
27
|
|
|
19
28
|
type RunAgentTurnOptions = {
|
|
20
29
|
agentName: string;
|
|
@@ -47,6 +56,7 @@ type RunAgentTurnOptions = {
|
|
|
47
56
|
dialogId?: string;
|
|
48
57
|
resultSummary?: string;
|
|
49
58
|
errorCode?: string;
|
|
59
|
+
toolEvidence?: TaskRunToolEvidence;
|
|
50
60
|
}) => Promise<void>;
|
|
51
61
|
scriptPathExists?: (path: string) => boolean;
|
|
52
62
|
fetchImpl?: typeof fetch;
|
|
@@ -148,6 +158,32 @@ function shouldTraceLocalTools(options: RunAgentTurnOptions) {
|
|
|
148
158
|
return Boolean(options.traceTools || options.env.NOLO_TRACE_TOOLS === "1");
|
|
149
159
|
}
|
|
150
160
|
|
|
161
|
+
function shouldCollectLocalToolEvidence(options: RunAgentTurnOptions) {
|
|
162
|
+
return Boolean(options.taskRunContext?.rowDbKey);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function summarizeLocalToolEvidence(events: LocalAgentToolEvent[]): TaskRunToolEvidence | undefined {
|
|
166
|
+
if (events.length === 0) return undefined;
|
|
167
|
+
const toolsUsed = [
|
|
168
|
+
...new Set(
|
|
169
|
+
events
|
|
170
|
+
.filter((event) => event.type === "tool-call")
|
|
171
|
+
.map((event) => event.toolName)
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
),
|
|
174
|
+
];
|
|
175
|
+
const rounds = new Set(events.map((event) => event.round)).size;
|
|
176
|
+
return {
|
|
177
|
+
toolsUsed,
|
|
178
|
+
summary: {
|
|
179
|
+
callCount: events.filter((event) => event.type === "tool-call").length,
|
|
180
|
+
resultCount: events.filter((event) => event.type === "tool-result").length,
|
|
181
|
+
errorCount: events.filter((event) => event.type === "tool-error").length,
|
|
182
|
+
rounds,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
151
187
|
const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 128;
|
|
152
188
|
|
|
153
189
|
function parseLocalToolRoundLimitError(error: unknown) {
|
|
@@ -260,6 +296,7 @@ async function defaultTaskRunRecorder(args: {
|
|
|
260
296
|
dialogId?: string;
|
|
261
297
|
resultSummary?: string;
|
|
262
298
|
errorCode?: string;
|
|
299
|
+
toolEvidence?: TaskRunToolEvidence;
|
|
263
300
|
}) {
|
|
264
301
|
const context = args.options.taskRunContext;
|
|
265
302
|
if (!context?.rowDbKey) return;
|
|
@@ -298,6 +335,12 @@ async function defaultTaskRunRecorder(args: {
|
|
|
298
335
|
...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
|
|
299
336
|
...(args.resultSummary ? ["--result-summary", args.resultSummary.slice(0, 240)] : []),
|
|
300
337
|
...(args.errorCode ? ["--error-code", args.errorCode] : []),
|
|
338
|
+
...(args.toolEvidence?.toolsUsed?.length
|
|
339
|
+
? ["--tools-used", args.toolEvidence.toolsUsed.join(",")]
|
|
340
|
+
: []),
|
|
341
|
+
...(args.toolEvidence?.summary
|
|
342
|
+
? ["--tool-trace-summary", JSON.stringify(args.toolEvidence.summary)]
|
|
343
|
+
: []),
|
|
301
344
|
],
|
|
302
345
|
});
|
|
303
346
|
if (context.workItemId && args.dialogId) {
|
|
@@ -346,6 +389,7 @@ async function recordTaskRunOutcome(args: {
|
|
|
346
389
|
dialogId?: string;
|
|
347
390
|
resultSummary?: string;
|
|
348
391
|
errorCode?: string;
|
|
392
|
+
toolEvidence?: TaskRunToolEvidence;
|
|
349
393
|
}) {
|
|
350
394
|
if (!args.options.taskRunContext?.rowDbKey) return;
|
|
351
395
|
try {
|
|
@@ -594,6 +638,7 @@ async function runLocalAgentTurnWithAutoRoundExpansion(
|
|
|
594
638
|
errorCode: "LOCAL_AGENT_FAILED",
|
|
595
639
|
resultSummary:
|
|
596
640
|
lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError),
|
|
641
|
+
toolEvidence: lastResult.toolEvidence,
|
|
597
642
|
});
|
|
598
643
|
}
|
|
599
644
|
return lastResult ?? { exitCode: 1 };
|
|
@@ -610,7 +655,10 @@ async function runLocalAgentTurnForCli(
|
|
|
610
655
|
}
|
|
611
656
|
|
|
612
657
|
options.output.write(`\n${options.agentName} -> working locally...\n`);
|
|
658
|
+
const toolEvents: LocalAgentToolEvent[] = [];
|
|
613
659
|
try {
|
|
660
|
+
const traceLocalTools = shouldTraceLocalTools(options);
|
|
661
|
+
const collectToolEvidence = shouldCollectLocalToolEvidence(options);
|
|
614
662
|
const result = await runLocalAgentTurn({
|
|
615
663
|
adapter,
|
|
616
664
|
agentRef: options.agentKey,
|
|
@@ -626,8 +674,13 @@ async function runLocalAgentTurnForCli(
|
|
|
626
674
|
background: options.background,
|
|
627
675
|
noStream: options.noStream,
|
|
628
676
|
maxToolRounds: options.maxToolRounds,
|
|
629
|
-
...(
|
|
630
|
-
? {
|
|
677
|
+
...(traceLocalTools || collectToolEvidence
|
|
678
|
+
? {
|
|
679
|
+
onToolEvent: (event) => {
|
|
680
|
+
if (collectToolEvidence) toolEvents.push(event);
|
|
681
|
+
if (traceLocalTools) options.output.write(formatToolTraceEvent(event));
|
|
682
|
+
},
|
|
683
|
+
}
|
|
631
684
|
: {}),
|
|
632
685
|
});
|
|
633
686
|
const content = result.content.trim();
|
|
@@ -641,6 +694,7 @@ async function runLocalAgentTurnForCli(
|
|
|
641
694
|
status: "completed",
|
|
642
695
|
dialogId: result.dialogId,
|
|
643
696
|
resultSummary: content || undefined,
|
|
697
|
+
toolEvidence: summarizeLocalToolEvidence(toolEvents),
|
|
644
698
|
});
|
|
645
699
|
return { exitCode: 0, dialogId: result.dialogId };
|
|
646
700
|
} catch (error) {
|
|
@@ -651,7 +705,7 @@ async function runLocalAgentTurnForCli(
|
|
|
651
705
|
}\n`
|
|
652
706
|
);
|
|
653
707
|
}
|
|
654
|
-
return { exitCode: 1, localError: error };
|
|
708
|
+
return { exitCode: 1, localError: error, toolEvidence: summarizeLocalToolEvidence(toolEvents) };
|
|
655
709
|
}
|
|
656
710
|
}
|
|
657
711
|
|
package/package.json
CHANGED
package/taskRunCommand.ts
CHANGED
|
@@ -58,6 +58,16 @@ function csv(raw: string) {
|
|
|
58
58
|
return raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function optionalJson(args: string[], flag: string) {
|
|
62
|
+
const value = optional(args, flag);
|
|
63
|
+
if (!value) return undefined;
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(value);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`${flag} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
function resolveServerUrl(args: string[], env: EnvLike) {
|
|
62
72
|
return (
|
|
63
73
|
readOption(args, "--server-url") ||
|
|
@@ -107,6 +117,23 @@ function buildBody(command: string, args: string[]) {
|
|
|
107
117
|
role: optional(args, "--role"),
|
|
108
118
|
};
|
|
109
119
|
}
|
|
120
|
+
if (command === "record-agent-run" || command === "recordAgentRun") {
|
|
121
|
+
const toolTraceSummary = optionalJson(args, "--tool-trace-summary");
|
|
122
|
+
return {
|
|
123
|
+
action: "recordAgentRun",
|
|
124
|
+
dbKey,
|
|
125
|
+
...(optional(args, "--run-id") ? { runId: optional(args, "--run-id") } : {}),
|
|
126
|
+
agentKey: required(args, "--agent-key"),
|
|
127
|
+
...(optional(args, "--role") ? { role: optional(args, "--role") } : {}),
|
|
128
|
+
runStatus: required(args, "--run-status"),
|
|
129
|
+
...(optional(args, "--work-item-id") ? { workItemId: optional(args, "--work-item-id") } : {}),
|
|
130
|
+
...(optional(args, "--dialog-id") ? { dialogId: optional(args, "--dialog-id") } : {}),
|
|
131
|
+
...(optional(args, "--result-summary") ? { resultSummary: optional(args, "--result-summary") } : {}),
|
|
132
|
+
...(optional(args, "--error-code") ? { errorCode: optional(args, "--error-code") } : {}),
|
|
133
|
+
...(optional(args, "--tools-used") ? { toolsUsed: csv(optional(args, "--tools-used")!) } : {}),
|
|
134
|
+
...(toolTraceSummary ? { toolTraceSummary } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
110
137
|
if (command === "set-blocker" || command === "setBlocker") {
|
|
111
138
|
return {
|
|
112
139
|
action: "setBlocker",
|
|
@@ -181,6 +208,7 @@ function usage() {
|
|
|
181
208
|
"Usage:",
|
|
182
209
|
" nolo task-run read-context --row-dbkey <row>",
|
|
183
210
|
" nolo task-run claim-work-item --row-dbkey <row> --work-item-id <id> --agent-key <agent> [--role <role>]",
|
|
211
|
+
" nolo task-run record-agent-run --row-dbkey <row> --agent-key <agent> --run-status <status> [--tools-used <csv>]",
|
|
184
212
|
" nolo task-run set-blocker --row-dbkey <row> [--work-item-id <id>] --layer <layer> --code <code> --message <message>",
|
|
185
213
|
" nolo task-run clear-blocker --row-dbkey <row> --blocker-id <id> [--work-item-id <id>]",
|
|
186
214
|
" nolo task-run submit-outcome --row-dbkey <row> --work-item-id <id> --summary <summary> [--dialog-id <id>]",
|