nolo-cli 0.1.20 → 0.1.22

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.
@@ -47,12 +47,30 @@ export interface SkillEvalConfig {
47
47
  cases: SkillEvalCase[];
48
48
  }
49
49
 
50
+ export interface WorkflowReferenceConfig {
51
+ version: "0.1";
52
+ kind: "workflow";
53
+ id?: string;
54
+ name: string;
55
+ description: string;
56
+ defaultAgent?: string;
57
+ inputs?: string[];
58
+ recommendedTools?: string[];
59
+ requiredTools?: string[];
60
+ requiredOutputs?: string[];
61
+ gates?: string[];
62
+ budgetTier?: SkillBudgetTier;
63
+ contextStrategy?: string;
64
+ failureProtocol?: string;
65
+ }
66
+
50
67
  export interface PageSkillMetadata {
51
68
  kind?: SkillDocKind;
52
69
  requiredSkills?: string[];
53
70
  recommendedSkills?: string[];
54
71
  skillConfig?: SkillDocConfig;
55
72
  evalConfig?: SkillEvalConfig;
73
+ workflowConfig?: WorkflowReferenceConfig;
56
74
  }
57
75
 
58
76
  export interface ParsedSkillDocProtocol {
@@ -71,6 +89,7 @@ export interface ParsedExternalSkillMarkdown {
71
89
 
72
90
  const SKILL_CONFIG_BLOCK = "skill-config";
73
91
  const EVAL_CONFIG_BLOCK = "eval-config";
92
+ const WORKFLOW_CONFIG_BLOCK = "workflow-config";
74
93
 
75
94
  const normalizeStringArray = (value: unknown): string[] | undefined => {
76
95
  if (!Array.isArray(value)) return undefined;
@@ -200,6 +219,49 @@ const normalizeEvalConfig = (value: unknown): SkillEvalConfig | undefined => {
200
219
  : undefined;
201
220
  };
202
221
 
222
+ const normalizeWorkflowConfig = (value: unknown): WorkflowReferenceConfig | undefined => {
223
+ if (!value || typeof value !== "object") return undefined;
224
+ const record = value as Record<string, unknown>;
225
+ const name =
226
+ typeof record.name === "string" && record.name.trim()
227
+ ? record.name.trim()
228
+ : "";
229
+ const description =
230
+ typeof record.description === "string" && record.description.trim()
231
+ ? record.description.trim()
232
+ : "";
233
+ if (!name || !description) return undefined;
234
+ const budgetTier = normalizeSkillEnumValue("budgetTier", record.budgetTier);
235
+ return {
236
+ version: "0.1",
237
+ kind: "workflow",
238
+ id:
239
+ typeof record.id === "string" && record.id.trim()
240
+ ? record.id.trim()
241
+ : undefined,
242
+ name,
243
+ description,
244
+ defaultAgent:
245
+ typeof record.defaultAgent === "string" && record.defaultAgent.trim()
246
+ ? record.defaultAgent.trim()
247
+ : undefined,
248
+ inputs: normalizeStringArray(record.inputs),
249
+ recommendedTools: normalizeStringArray(record.recommendedTools),
250
+ requiredTools: normalizeStringArray(record.requiredTools),
251
+ requiredOutputs: normalizeStringArray(record.requiredOutputs),
252
+ gates: normalizeStringArray(record.gates),
253
+ budgetTier,
254
+ contextStrategy:
255
+ typeof record.contextStrategy === "string" && record.contextStrategy.trim()
256
+ ? record.contextStrategy.trim()
257
+ : undefined,
258
+ failureProtocol:
259
+ typeof record.failureProtocol === "string" && record.failureProtocol.trim()
260
+ ? record.failureProtocol.trim()
261
+ : undefined,
262
+ };
263
+ };
264
+
203
265
  const normalizePageSkillMetadata = (
204
266
  value: unknown,
205
267
  fallbackTools?: string[]
@@ -214,17 +276,22 @@ const normalizePageSkillMetadata = (
214
276
  recommendedSkills: normalizeStringArray(record.recommendedSkills),
215
277
  skillConfig: normalizeSkillConfig(record.skillConfig, fallbackTools),
216
278
  evalConfig: normalizeEvalConfig(record.evalConfig),
279
+ workflowConfig: normalizeWorkflowConfig(record.workflowConfig),
217
280
  };
218
281
 
219
282
  if (meta.skillConfig && !meta.kind) {
220
283
  meta.kind = "skill";
221
284
  }
285
+ if (meta.workflowConfig && !meta.kind) {
286
+ meta.kind = "instruction";
287
+ }
222
288
 
223
289
  return meta.kind ||
224
290
  meta.requiredSkills ||
225
291
  meta.recommendedSkills ||
226
292
  meta.skillConfig ||
227
- meta.evalConfig
293
+ meta.evalConfig ||
294
+ meta.workflowConfig
228
295
  ? meta
229
296
  : undefined;
230
297
  };
@@ -268,9 +335,13 @@ export const parseSkillDocProtocol = (
268
335
  const source = typeof markdown === "string" ? markdown : "";
269
336
  const skillBlock = parseYamlObject(extractCommentBlock(source, SKILL_CONFIG_BLOCK));
270
337
  const evalBlock = parseYamlObject(extractCommentBlock(source, EVAL_CONFIG_BLOCK));
338
+ const workflowBlock = parseYamlObject(extractCommentBlock(source, WORKFLOW_CONFIG_BLOCK));
271
339
  const cleanedContent = removeCommentBlock(
272
- removeCommentBlock(source, SKILL_CONFIG_BLOCK),
273
- EVAL_CONFIG_BLOCK
340
+ removeCommentBlock(
341
+ removeCommentBlock(source, SKILL_CONFIG_BLOCK),
342
+ EVAL_CONFIG_BLOCK
343
+ ),
344
+ WORKFLOW_CONFIG_BLOCK
274
345
  )
275
346
  .replace(/\n{3,}/g, "\n\n")
276
347
  .trim();
@@ -292,6 +363,7 @@ export const parseSkillDocProtocol = (
292
363
  }
293
364
  : {}),
294
365
  ...(evalBlock ? { evalConfig: evalBlock } : {}),
366
+ ...(workflowBlock ? { workflowConfig: workflowBlock } : {}),
295
367
  },
296
368
  fallbackTools
297
369
  );
@@ -364,15 +436,35 @@ export const buildEvalConfigComment = (config: SkillEvalConfig): string =>
364
436
  cases: config.cases,
365
437
  })}\n-->`;
366
438
 
439
+ export const buildWorkflowConfigComment = (config: WorkflowReferenceConfig): string =>
440
+ `<!-- ${WORKFLOW_CONFIG_BLOCK}\n${yamlBlock({
441
+ version: config.version,
442
+ kind: config.kind,
443
+ ...(config.id ? { id: config.id } : {}),
444
+ name: config.name,
445
+ description: config.description,
446
+ ...(config.defaultAgent ? { defaultAgent: config.defaultAgent } : {}),
447
+ ...(config.inputs?.length ? { inputs: config.inputs } : {}),
448
+ ...(config.recommendedTools?.length ? { recommendedTools: config.recommendedTools } : {}),
449
+ ...(config.requiredTools?.length ? { requiredTools: config.requiredTools } : {}),
450
+ ...(config.requiredOutputs?.length ? { requiredOutputs: config.requiredOutputs } : {}),
451
+ ...(config.gates?.length ? { gates: config.gates } : {}),
452
+ ...(config.budgetTier ? { budgetTier: config.budgetTier } : {}),
453
+ ...(config.contextStrategy ? { contextStrategy: config.contextStrategy } : {}),
454
+ ...(config.failureProtocol ? { failureProtocol: config.failureProtocol } : {}),
455
+ })}\n-->`;
456
+
367
457
  export const buildSkillDocMarkdown = (options: {
368
458
  body?: string;
369
459
  skillConfig: SkillDocConfig;
370
460
  evalConfig?: SkillEvalConfig;
461
+ workflowConfig?: WorkflowReferenceConfig;
371
462
  }): string => {
372
463
  const sections = [
373
464
  options.body?.trim() || "",
374
465
  buildSkillConfigComment(options.skillConfig),
375
466
  options.evalConfig ? buildEvalConfigComment(options.evalConfig) : "",
467
+ options.workflowConfig ? buildWorkflowConfigComment(options.workflowConfig) : "",
376
468
  ].filter(Boolean);
377
469
  return sections.join("\n\n").trim();
378
470
  };
@@ -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;
@@ -7,6 +7,7 @@ import {
7
7
  runAgentTurn,
8
8
  shouldUseScriptBridge,
9
9
  } from "./agentRun";
10
+ import { NOLO_PROJECT_MANAGER_AGENT_KEY } from "../agentAliases";
10
11
 
11
12
  class CaptureOutput extends Writable {
12
13
  chunks: string[] = [];
@@ -452,6 +453,75 @@ describe("cli agent run client", () => {
452
453
  expect(output.text()).toContain("[nolo:tool] round 1 <- gitStatus (call-status)");
453
454
  });
454
455
 
456
+ test("passes local tool evidence to task-run writeback", async () => {
457
+ const output = new CaptureOutput();
458
+ const writebacks: any[] = [];
459
+ let completeCalls = 0;
460
+
461
+ const result = await runAgentTurn({
462
+ agentName: "frontend",
463
+ agentKey: "frontend-local",
464
+ serverUrl: "https://nolo.chat",
465
+ message: "inspect repo",
466
+ scriptDir: "C:/missing/scripts",
467
+ env: { AUTH_TOKEN: "token-123" },
468
+ output,
469
+ runtimeMode: "local",
470
+ taskRunContext: {
471
+ rowDbKey: "row-b2e06f801f-01TASK",
472
+ workItemId: "self",
473
+ },
474
+ taskRunRecorder: async (args) => {
475
+ writebacks.push(args);
476
+ },
477
+ localRuntimeAdapter: {
478
+ host: "cli",
479
+ capabilities: ["local-provider", "local-persistence", "local-tools"],
480
+ loadAgentConfig: async (agentRef) => ({
481
+ key: agentRef,
482
+ name: "Frontend",
483
+ prompt: "Fix UI",
484
+ model: "fake-local",
485
+ toolNames: ["gitStatus"],
486
+ }),
487
+ loadDialogHistory: async () => [],
488
+ saveTurn: async () => ({ dialogId: "dialog-local" }),
489
+ resolveProvider: async () => ({
490
+ model: "fake-local",
491
+ complete: async () => {
492
+ completeCalls += 1;
493
+ if (completeCalls === 1) {
494
+ return {
495
+ content: "",
496
+ model: "fake-local",
497
+ tool_calls: [{
498
+ id: "call-status",
499
+ type: "function",
500
+ function: { name: "gitStatus", arguments: "{}" },
501
+ }],
502
+ };
503
+ }
504
+ return { content: "done", model: "fake-local" };
505
+ },
506
+ }),
507
+ executeTool: async () => ({ content: "clean" }),
508
+ },
509
+ scriptPathExists: () => false,
510
+ });
511
+
512
+ expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
513
+ expect(writebacks).toHaveLength(1);
514
+ expect(writebacks[0].toolEvidence).toEqual({
515
+ toolsUsed: ["gitStatus"],
516
+ summary: {
517
+ callCount: 1,
518
+ resultCount: 1,
519
+ errorCount: 0,
520
+ rounds: 1,
521
+ },
522
+ });
523
+ });
524
+
455
525
  test("auto mode prefers a working local runtime before HTTP", async () => {
456
526
  const output = new CaptureOutput();
457
527
  const httpCalls: string[] = [];
@@ -502,7 +572,7 @@ describe("cli agent run client", () => {
502
572
 
503
573
  const result = await runAgentTurn({
504
574
  agentName: "pm",
505
- agentKey: "nolo-project-manager",
575
+ agentKey: "agent-custom-platform-tools",
506
576
  serverUrl: "https://nolo.chat",
507
577
  message: "write task rows",
508
578
  scriptDir: "C:/missing/scripts",
@@ -539,13 +609,60 @@ describe("cli agent run client", () => {
539
609
 
540
610
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server" });
541
611
  expect(httpCalls).toHaveLength(1);
542
- expect(httpCalls[0]?.body.agentKey).toBe("nolo-project-manager");
612
+ expect(httpCalls[0]?.body.agentKey).toBe("agent-custom-platform-tools");
543
613
  expect(output.text()).toContain("auto runtime: skipping local runtime");
544
614
  expect(output.text()).toContain("queryTableRows, addTableRow, updateTableRow");
545
615
  expect(output.text()).toContain("pm -> working");
546
616
  expect(output.text()).toContain("pm > server ok");
547
617
  });
548
618
 
619
+ test("auto mode skips known platform agents when local config cannot be read", async () => {
620
+ const output = new CaptureOutput();
621
+ const httpCalls: Array<{ url: string; body: any }> = [];
622
+
623
+ const result = await runAgentTurn({
624
+ agentName: "nolo-project-manager",
625
+ agentKey: NOLO_PROJECT_MANAGER_AGENT_KEY,
626
+ serverUrl: "https://us.nolo.chat",
627
+ message: "write task rows",
628
+ scriptDir: "C:/missing/scripts",
629
+ env: { AUTH_TOKEN: "token-123", OPENAI_API_KEY: "local-provider-present" },
630
+ output,
631
+ runtimeMode: "auto",
632
+ localRuntimeAdapter: {
633
+ host: "cli",
634
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
635
+ loadAgentConfig: async () => {
636
+ throw new Error("Database failed to open: LOCK");
637
+ },
638
+ loadDialogHistory: async () => {
639
+ throw new Error("local runtime should be skipped");
640
+ },
641
+ saveTurn: async () => {
642
+ throw new Error("local runtime should be skipped");
643
+ },
644
+ resolveProvider: async () => {
645
+ throw new Error("local provider should be skipped");
646
+ },
647
+ executeTool: async () => {
648
+ throw new Error("local tools should be skipped");
649
+ },
650
+ },
651
+ scriptPathExists: () => false,
652
+ fetchImpl: async (url, init) => {
653
+ httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
654
+ return Response.json({ content: "server ok", dialogId: "dialog-server" });
655
+ },
656
+ });
657
+
658
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server" });
659
+ expect(httpCalls).toHaveLength(1);
660
+ expect(httpCalls[0]?.body.agentKey).toBe(NOLO_PROJECT_MANAGER_AGENT_KEY);
661
+ expect(output.text()).toContain("known platform agent");
662
+ expect(output.text()).toContain("nolo-project-manager -> working");
663
+ expect(output.text()).toContain("nolo-project-manager > server ok");
664
+ });
665
+
549
666
  test("builds the default local adapter when env requests local mode", async () => {
550
667
  const output = new CaptureOutput();
551
668
  const builtModes: string[] = [];
@@ -1,6 +1,13 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { runLocalAgentTurn } from "../agentRuntimeLocal";
4
+ import {
5
+ FRONTEND_IMPLEMENTER_AGENT_KEY,
6
+ NOLO_FULLSTACK_AGENT_KEY,
7
+ NOLO_PROJECT_MANAGER_AGENT_KEY,
8
+ NOLO_REVIEW_AGENT_KEY,
9
+ NOLO_SENIOR_FULLSTACK_AGENT_KEY,
10
+ } from "../agentAliases";
4
11
  import type { LocalAgentToolEvent } from "../agent-runtime/localLoop";
5
12
  import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
6
13
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
@@ -15,6 +22,15 @@ type OutputLike = {
15
22
 
16
23
  type TaskRunAgentRole = "pm" | "frontend" | "fullstack" | "reviewer" | "codex";
17
24
  type TaskRunReviewStatus = "passed" | "needs_changes" | "blocked";
25
+ type TaskRunToolEvidence = {
26
+ toolsUsed: string[];
27
+ summary: {
28
+ callCount: number;
29
+ resultCount: number;
30
+ errorCount: number;
31
+ rounds: number;
32
+ };
33
+ };
18
34
 
19
35
  type RunAgentTurnOptions = {
20
36
  agentName: string;
@@ -47,6 +63,7 @@ type RunAgentTurnOptions = {
47
63
  dialogId?: string;
48
64
  resultSummary?: string;
49
65
  errorCode?: string;
66
+ toolEvidence?: TaskRunToolEvidence;
50
67
  }) => Promise<void>;
51
68
  scriptPathExists?: (path: string) => boolean;
52
69
  fetchImpl?: typeof fetch;
@@ -74,6 +91,38 @@ const SERVER_PLATFORM_TOOL_NAMES = new Set([
74
91
  "updateTableRows",
75
92
  ]);
76
93
 
94
+ const KNOWN_SERVER_PLATFORM_AGENT_KEYS = new Set([
95
+ FRONTEND_IMPLEMENTER_AGENT_KEY,
96
+ NOLO_FULLSTACK_AGENT_KEY,
97
+ NOLO_PROJECT_MANAGER_AGENT_KEY,
98
+ NOLO_REVIEW_AGENT_KEY,
99
+ NOLO_SENIOR_FULLSTACK_AGENT_KEY,
100
+ ]);
101
+
102
+ const KNOWN_SERVER_PLATFORM_AGENT_ALIASES = new Set([
103
+ "code-review",
104
+ "frontend",
105
+ "frontend-agent",
106
+ "frontend-implementer",
107
+ "full-stack",
108
+ "fullstack",
109
+ "nolo code review",
110
+ "nolo frontend",
111
+ "nolo fullstack",
112
+ "nolo project manager",
113
+ "nolo reviewer",
114
+ "nolo-code-review",
115
+ "nolo-frontend",
116
+ "nolo-fullstack",
117
+ "nolo-pm",
118
+ "nolo-project-manager",
119
+ "nolo-reviewer",
120
+ "pm",
121
+ "project-manager",
122
+ "review",
123
+ "reviewer",
124
+ ]);
125
+
77
126
  export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
78
127
  return !decision.hasAuthToken && decision.scriptPathExists;
79
128
  }
@@ -82,6 +131,16 @@ export function findServerPlatformTools(toolNames?: string[]) {
82
131
  if (!Array.isArray(toolNames)) return [];
83
132
  return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
84
133
  }
134
+
135
+ function normalizeAgentRef(ref?: string) {
136
+ return ref?.trim().toLowerCase().replace(/\s+/g, " ");
137
+ }
138
+
139
+ function isKnownServerPlatformAgent(options: RunAgentTurnOptions) {
140
+ if (KNOWN_SERVER_PLATFORM_AGENT_KEYS.has(options.agentKey)) return true;
141
+ const normalizedKey = normalizeAgentRef(options.agentKey);
142
+ return Boolean(normalizedKey && KNOWN_SERVER_PLATFORM_AGENT_ALIASES.has(normalizedKey));
143
+ }
85
144
 
86
145
  function resolveAuthToken(env: EnvLike) {
87
146
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
@@ -116,6 +175,13 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
116
175
  }
117
176
 
118
177
  async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
178
+ if (isKnownServerPlatformAgent(options)) {
179
+ options.output.write(
180
+ `[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a known platform agent. ` +
181
+ "Use --local explicitly to force local workspace tools.\n"
182
+ );
183
+ return true;
184
+ }
119
185
  const adapter = resolveLocalRuntimeAdapter(options);
120
186
  if (!adapter) return false;
121
187
  let agentConfig;
@@ -148,6 +214,32 @@ function shouldTraceLocalTools(options: RunAgentTurnOptions) {
148
214
  return Boolean(options.traceTools || options.env.NOLO_TRACE_TOOLS === "1");
149
215
  }
150
216
 
217
+ function shouldCollectLocalToolEvidence(options: RunAgentTurnOptions) {
218
+ return Boolean(options.taskRunContext?.rowDbKey);
219
+ }
220
+
221
+ function summarizeLocalToolEvidence(events: LocalAgentToolEvent[]): TaskRunToolEvidence | undefined {
222
+ if (events.length === 0) return undefined;
223
+ const toolsUsed = [
224
+ ...new Set(
225
+ events
226
+ .filter((event) => event.type === "tool-call")
227
+ .map((event) => event.toolName)
228
+ .filter(Boolean)
229
+ ),
230
+ ];
231
+ const rounds = new Set(events.map((event) => event.round)).size;
232
+ return {
233
+ toolsUsed,
234
+ summary: {
235
+ callCount: events.filter((event) => event.type === "tool-call").length,
236
+ resultCount: events.filter((event) => event.type === "tool-result").length,
237
+ errorCount: events.filter((event) => event.type === "tool-error").length,
238
+ rounds,
239
+ },
240
+ };
241
+ }
242
+
151
243
  const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 128;
152
244
 
153
245
  function parseLocalToolRoundLimitError(error: unknown) {
@@ -260,6 +352,7 @@ async function defaultTaskRunRecorder(args: {
260
352
  dialogId?: string;
261
353
  resultSummary?: string;
262
354
  errorCode?: string;
355
+ toolEvidence?: TaskRunToolEvidence;
263
356
  }) {
264
357
  const context = args.options.taskRunContext;
265
358
  if (!context?.rowDbKey) return;
@@ -298,6 +391,12 @@ async function defaultTaskRunRecorder(args: {
298
391
  ...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
299
392
  ...(args.resultSummary ? ["--result-summary", args.resultSummary.slice(0, 240)] : []),
300
393
  ...(args.errorCode ? ["--error-code", args.errorCode] : []),
394
+ ...(args.toolEvidence?.toolsUsed?.length
395
+ ? ["--tools-used", args.toolEvidence.toolsUsed.join(",")]
396
+ : []),
397
+ ...(args.toolEvidence?.summary
398
+ ? ["--tool-trace-summary", JSON.stringify(args.toolEvidence.summary)]
399
+ : []),
301
400
  ],
302
401
  });
303
402
  if (context.workItemId && args.dialogId) {
@@ -346,6 +445,7 @@ async function recordTaskRunOutcome(args: {
346
445
  dialogId?: string;
347
446
  resultSummary?: string;
348
447
  errorCode?: string;
448
+ toolEvidence?: TaskRunToolEvidence;
349
449
  }) {
350
450
  if (!args.options.taskRunContext?.rowDbKey) return;
351
451
  try {
@@ -594,6 +694,7 @@ async function runLocalAgentTurnWithAutoRoundExpansion(
594
694
  errorCode: "LOCAL_AGENT_FAILED",
595
695
  resultSummary:
596
696
  lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError),
697
+ toolEvidence: lastResult.toolEvidence,
597
698
  });
598
699
  }
599
700
  return lastResult ?? { exitCode: 1 };
@@ -610,7 +711,10 @@ async function runLocalAgentTurnForCli(
610
711
  }
611
712
 
612
713
  options.output.write(`\n${options.agentName} -> working locally...\n`);
714
+ const toolEvents: LocalAgentToolEvent[] = [];
613
715
  try {
716
+ const traceLocalTools = shouldTraceLocalTools(options);
717
+ const collectToolEvidence = shouldCollectLocalToolEvidence(options);
614
718
  const result = await runLocalAgentTurn({
615
719
  adapter,
616
720
  agentRef: options.agentKey,
@@ -626,8 +730,13 @@ async function runLocalAgentTurnForCli(
626
730
  background: options.background,
627
731
  noStream: options.noStream,
628
732
  maxToolRounds: options.maxToolRounds,
629
- ...(shouldTraceLocalTools(options)
630
- ? { onToolEvent: (event) => options.output.write(formatToolTraceEvent(event)) }
733
+ ...(traceLocalTools || collectToolEvidence
734
+ ? {
735
+ onToolEvent: (event) => {
736
+ if (collectToolEvidence) toolEvents.push(event);
737
+ if (traceLocalTools) options.output.write(formatToolTraceEvent(event));
738
+ },
739
+ }
631
740
  : {}),
632
741
  });
633
742
  const content = result.content.trim();
@@ -641,6 +750,7 @@ async function runLocalAgentTurnForCli(
641
750
  status: "completed",
642
751
  dialogId: result.dialogId,
643
752
  resultSummary: content || undefined,
753
+ toolEvidence: summarizeLocalToolEvidence(toolEvents),
644
754
  });
645
755
  return { exitCode: 0, dialogId: result.dialogId };
646
756
  } catch (error) {
@@ -651,7 +761,7 @@ async function runLocalAgentTurnForCli(
651
761
  }\n`
652
762
  );
653
763
  }
654
- return { exitCode: 1, localError: error };
764
+ return { exitCode: 1, localError: error, toolEvidence: summarizeLocalToolEvidence(toolEvents) };
655
765
  }
656
766
  }
657
767
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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>]",