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.
- package/ai/skills/skillDocProtocol.ts +95 -3
- package/ai/taskRun/taskRunProtocol.ts +59 -0
- package/client/agentRun.test.ts +119 -2
- package/client/agentRun.ts +113 -3
- package/package.json +1 -1
- package/taskRunCommand.ts +28 -0
|
@@ -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(
|
|
273
|
-
|
|
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;
|
package/client/agentRun.test.ts
CHANGED
|
@@ -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: "
|
|
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("
|
|
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[] = [];
|
package/client/agentRun.ts
CHANGED
|
@@ -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
|
-
...(
|
|
630
|
-
? {
|
|
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
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>]",
|