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.
@@ -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;
@@ -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[] = [];
@@ -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
- ...(shouldTraceLocalTools(options)
630
- ? { onToolEvent: (event) => options.output.write(formatToolTraceEvent(event)) }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
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>]",