nolo-cli 0.1.21 → 0.1.23
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/agent-runtime/agentRecordConfig.ts +4 -0
- package/agent-runtime/hostAdapter.ts +2 -0
- package/agent-runtime/index.ts +7 -0
- package/agent-runtime/localLoop.ts +2 -0
- package/agent-runtime/platformChatProvider.ts +3 -0
- package/agent-runtime/runtimeToolPolicy.ts +92 -0
- package/agent-runtime/types.ts +42 -0
- package/agentRunCommand.ts +74 -1
- package/agentRuntimeCommands.ts +17 -89
- package/ai/agent/streamAgentChatTurn.ts +104 -20
- package/ai/chat/fetchUtils.native.ts +2 -0
- package/ai/chat/fetchUtils.ts +2 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
- package/ai/llm/kimi.ts +1 -1
- package/ai/llm/providers.ts +3 -0
- package/ai/llm/reasoningModels.ts +1 -0
- package/ai/skills/skillDocProtocol.ts +95 -3
- package/ai/taskRun/taskRunProtocol.ts +1 -0
- package/ai/tools/agent/agentTools.ts +17 -0
- package/ai/tools/agent/startAgentDialogTool.ts +53 -0
- package/ai/tools/modelUsageTools.ts +5 -0
- package/client/agentRun.test.ts +257 -7
- package/client/agentRun.ts +133 -34
- package/client/localRuntimeAdapter.test.ts +2 -0
- package/client/localRuntimeAdapter.ts +15 -2
- package/database/actions/common.ts +4 -3
- package/database/config.ts +19 -0
- package/machineCommands.ts +400 -45
- package/package.json +4 -2
- package/render/canvas/canvasEditContext.ts +127 -0
- package/render/canvas/canvasRuntime.ts +57 -0
- package/render/canvas/canvasSnapshotParser.ts +76 -0
- package/render/canvas/canvasTree.ts +308 -0
- package/render/canvas/types.ts +46 -0
- package/render/layout/deleteBehavior.ts +52 -0
- package/render/layout/mainLayoutSidebar.ts +17 -0
- package/render/layout/mainLayoutViewMode.ts +56 -0
- package/render/layout/topbarUtils.ts +87 -0
- package/render/layout/useDevReloadPending.ts +30 -0
- package/render/page/createPageAction.ts +183 -0
- package/render/page/docSlice.ts +468 -0
- package/render/page/server/createPage.ts +174 -0
- package/render/page/server/handleCreatePage.ts +91 -0
- package/render/page/server/index.ts +4 -0
- package/render/page/types.ts +17 -0
- package/render/page/useKeyboardSave.ts +48 -0
- package/render/styles/zIndex.ts +12 -0
- package/render/surf/WeatherIconStyles.ts +17 -0
- package/render/surf/color.ts +9 -0
- package/render/surf/config.ts +46 -0
- package/render/surf/screens/style.ts +1 -0
- package/render/surf/styles/ToggleButtonStyles.ts +8 -0
- package/render/surf/utils/groupedWeatherData.ts +32 -0
- package/render/surf/weatherUtils.ts +50 -0
- package/render/table/activityColumns.ts +6 -0
- package/render/table/createTableAction.ts +270 -0
- package/render/table/deleteTableAction.ts +129 -0
- package/render/table/fetchAndCacheTableRows.ts +174 -0
- package/render/table/tableSlice.ts +1106 -0
- package/render/table/tableView.ts +289 -0
- package/render/table/toolValueUtils.ts +363 -0
- package/render/table/types.ts +252 -0
- package/render/table/useCreateTable.ts +72 -0
- package/render/table/useTable.ts +61 -0
- package/render/table/utils/tableSerialization.ts +50 -0
- package/render/web/elements/artifactPreviewCode.ts +43 -0
- package/render/web/elements/artifactRuntimePreload.ts +52 -0
- package/render/web/elements/codeBlockAutoPreview.ts +10 -0
- package/render/web/elements/mermaidPreview.ts +21 -0
- package/render/web/ui/useInlineEdit.ts +135 -0
- package/tableCommands.ts +42 -5
|
@@ -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
|
};
|
|
@@ -193,6 +193,7 @@ export type TaskRunControlPolicy = {
|
|
|
193
193
|
autonomy?: TaskRunAutonomy;
|
|
194
194
|
nextWakeAt?: string;
|
|
195
195
|
pausedReason?: string;
|
|
196
|
+
runtimeToolPolicy?: Record<string, unknown>;
|
|
196
197
|
budget?: {
|
|
197
198
|
dailyUsdLimit?: number;
|
|
198
199
|
modelBudget?: Record<string, TaskRunBudgetRule>;
|
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
streamParallelAgentsFunctionSchema,
|
|
29
29
|
streamParallelAgentsFunc,
|
|
30
30
|
} from "./streamParallelAgentsTool";
|
|
31
|
+
import {
|
|
32
|
+
startAgentDialogFunctionSchema,
|
|
33
|
+
startAgentDialogFunc,
|
|
34
|
+
} from "./startAgentDialogTool";
|
|
31
35
|
import {
|
|
32
36
|
createDialogFunctionSchema,
|
|
33
37
|
createDialogFunc,
|
|
@@ -151,6 +155,19 @@ export const agentToolDefinitions: ToolDefinition[] = [
|
|
|
151
155
|
behavior: "orchestrator",
|
|
152
156
|
uiGroup: "agent",
|
|
153
157
|
},
|
|
158
|
+
{
|
|
159
|
+
id: "startAgentDialog",
|
|
160
|
+
schema: startAgentDialogFunctionSchema,
|
|
161
|
+
executor: startAgentDialogFunc,
|
|
162
|
+
description: {
|
|
163
|
+
name: "startAgentDialog",
|
|
164
|
+
description:
|
|
165
|
+
"启动一个子 Agent dialog,返回 childDialogId,供调用方后续查询、接管或投影状态。",
|
|
166
|
+
category: "计划与编排",
|
|
167
|
+
},
|
|
168
|
+
behavior: "orchestrator",
|
|
169
|
+
uiGroup: "agent",
|
|
170
|
+
},
|
|
154
171
|
{
|
|
155
172
|
id: "createDialog",
|
|
156
173
|
schema: createDialogFunctionSchema,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const startAgentDialogFunctionSchema = {
|
|
2
|
+
name: "startAgentDialog",
|
|
3
|
+
description:
|
|
4
|
+
"启动一个子 Agent dialog 并立即返回 childDialogId。用于通用 agent 派发/交接,不等待子 Agent 完成,也不替调用方决定任务状态。",
|
|
5
|
+
parameters: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
targetAgentKey: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "要启动的目标 Agent key。",
|
|
11
|
+
},
|
|
12
|
+
message: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "发送给目标 Agent 的自然语言任务或上下文。",
|
|
15
|
+
},
|
|
16
|
+
parentDialogId: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "可选。父 dialogId;未提供时使用当前 dialogId。",
|
|
19
|
+
},
|
|
20
|
+
subjectRefs: {
|
|
21
|
+
type: "array",
|
|
22
|
+
description: "可选。与本次子 dialog 相关的业务对象引用,保持行业中立。",
|
|
23
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
kind: { type: "string" },
|
|
27
|
+
id: { type: "string" },
|
|
28
|
+
role: { type: "string" },
|
|
29
|
+
},
|
|
30
|
+
required: ["kind", "id"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
idempotencyKey: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "可选。调用方用于去重的稳定 key;当前版本仅透传给运行上下文。",
|
|
36
|
+
},
|
|
37
|
+
serverBase: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description:
|
|
40
|
+
"可选。目标 Agent 所在的 nolo server origin;跨域目标必须由服务端 AGENT_TOOL_ALLOWED_SERVER_BASES 放行。",
|
|
41
|
+
},
|
|
42
|
+
timeoutMs: {
|
|
43
|
+
type: "number",
|
|
44
|
+
description: "可选。子 Agent background run 的执行超时预算(毫秒)。",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ["targetAgentKey", "message"],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export async function startAgentDialogFunc(): Promise<never> {
|
|
52
|
+
throw new Error("startAgentDialog is a server-side agent tool. Use it through /api/agent/run.");
|
|
53
|
+
}
|
|
@@ -179,6 +179,11 @@ export const notifyUserFunctionSchema = {
|
|
|
179
179
|
type: "string",
|
|
180
180
|
description: "可选跳转链接。",
|
|
181
181
|
},
|
|
182
|
+
targetUserId: {
|
|
183
|
+
type: "string",
|
|
184
|
+
description:
|
|
185
|
+
"可选目标用户 ID。默认通知当前用户;只有受信任的系统 agent 或管理员可以通知其他用户。",
|
|
186
|
+
},
|
|
182
187
|
},
|
|
183
188
|
required: ["title", "message"],
|
|
184
189
|
},
|
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[] = [];
|
|
@@ -131,6 +132,12 @@ describe("cli agent run client", () => {
|
|
|
131
132
|
expect(requests[0]?.body.userInput).toContain("rowDbKey: row-b2e06f801f-01TASK");
|
|
132
133
|
expect(requests[0]?.body.userInput).toContain("workItemId: frontend-filter");
|
|
133
134
|
expect(requests[0]?.body.userInput).toContain("User task:\nFix the filter UI");
|
|
135
|
+
expect(requests[0]?.body.runtimeContext.taskRun).toEqual({
|
|
136
|
+
rowDbKey: "row-b2e06f801f-01TASK",
|
|
137
|
+
taskRunId: "taskrun-1",
|
|
138
|
+
workItemId: "frontend-filter",
|
|
139
|
+
artifactIds: ["artifact-1"],
|
|
140
|
+
});
|
|
134
141
|
});
|
|
135
142
|
|
|
136
143
|
test("records task-run completion after an HTTP agent run returns a dialog", async () => {
|
|
@@ -242,6 +249,42 @@ describe("cli agent run client", () => {
|
|
|
242
249
|
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
|
|
243
250
|
});
|
|
244
251
|
|
|
252
|
+
test("records background task-run handoff as running instead of completed", async () => {
|
|
253
|
+
const output = new CaptureOutput();
|
|
254
|
+
const writebacks: any[] = [];
|
|
255
|
+
|
|
256
|
+
const result = await runAgentTurn({
|
|
257
|
+
agentName: "frontend-implementer",
|
|
258
|
+
agentKey: "agent-frontend",
|
|
259
|
+
serverUrl: "https://nolo.chat",
|
|
260
|
+
message: "Fix settings style FOUC",
|
|
261
|
+
scriptDir: "C:/missing/scripts",
|
|
262
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
263
|
+
runtimeMode: "server",
|
|
264
|
+
background: true,
|
|
265
|
+
noStream: true,
|
|
266
|
+
taskRunContext: {
|
|
267
|
+
rowDbKey: "row-b2e06f801f-01TASK",
|
|
268
|
+
workItemId: "self",
|
|
269
|
+
},
|
|
270
|
+
taskRunRecorder: async (args) => {
|
|
271
|
+
writebacks.push(args);
|
|
272
|
+
},
|
|
273
|
+
output,
|
|
274
|
+
scriptPathExists: () => false,
|
|
275
|
+
fetchImpl: async () => {
|
|
276
|
+
return Response.json({ dialogId: "dialog-bg", status: "pending" }, { status: 202 });
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-bg" });
|
|
281
|
+
expect(writebacks).toHaveLength(1);
|
|
282
|
+
expect(writebacks[0]).toMatchObject({
|
|
283
|
+
status: "running",
|
|
284
|
+
dialogId: "dialog-bg",
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
245
288
|
test("runs forced local turns through the injected runtime adapter without HTTP", async () => {
|
|
246
289
|
const output = new CaptureOutput();
|
|
247
290
|
const result = await runAgentTurn({
|
|
@@ -521,6 +564,58 @@ describe("cli agent run client", () => {
|
|
|
521
564
|
});
|
|
522
565
|
});
|
|
523
566
|
|
|
567
|
+
test("records local background task-run handoff as running instead of completed", async () => {
|
|
568
|
+
const output = new CaptureOutput();
|
|
569
|
+
const writebacks: any[] = [];
|
|
570
|
+
|
|
571
|
+
const result = await runAgentTurn({
|
|
572
|
+
agentName: "frontend",
|
|
573
|
+
agentKey: "frontend-local",
|
|
574
|
+
serverUrl: "https://nolo.chat",
|
|
575
|
+
message: "inspect repo in background",
|
|
576
|
+
scriptDir: "C:/missing/scripts",
|
|
577
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
578
|
+
output,
|
|
579
|
+
runtimeMode: "local",
|
|
580
|
+
background: true,
|
|
581
|
+
taskRunContext: {
|
|
582
|
+
rowDbKey: "row-b2e06f801f-01TASK",
|
|
583
|
+
workItemId: "self",
|
|
584
|
+
},
|
|
585
|
+
taskRunRecorder: async (args) => {
|
|
586
|
+
writebacks.push(args);
|
|
587
|
+
},
|
|
588
|
+
localRuntimeAdapter: {
|
|
589
|
+
host: "cli",
|
|
590
|
+
capabilities: ["local-provider", "local-persistence"],
|
|
591
|
+
loadAgentConfig: async (agentRef) => ({
|
|
592
|
+
key: agentRef,
|
|
593
|
+
name: "Frontend",
|
|
594
|
+
prompt: "Fix UI",
|
|
595
|
+
model: "fake-local",
|
|
596
|
+
}),
|
|
597
|
+
loadDialogHistory: async () => [],
|
|
598
|
+
saveTurn: async () => ({ dialogId: "dialog-local-bg" }),
|
|
599
|
+
resolveProvider: async () => ({
|
|
600
|
+
model: "fake-local",
|
|
601
|
+
complete: async () => ({ content: "accepted locally", model: "fake-local" }),
|
|
602
|
+
}),
|
|
603
|
+
executeTool: async () => {
|
|
604
|
+
throw new Error("no tools expected");
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
scriptPathExists: () => false,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-bg" });
|
|
611
|
+
expect(writebacks).toHaveLength(1);
|
|
612
|
+
expect(writebacks[0]).toMatchObject({
|
|
613
|
+
status: "running",
|
|
614
|
+
dialogId: "dialog-local-bg",
|
|
615
|
+
});
|
|
616
|
+
expect(writebacks[0].resultSummary).toBeUndefined();
|
|
617
|
+
});
|
|
618
|
+
|
|
524
619
|
test("auto mode prefers a working local runtime before HTTP", async () => {
|
|
525
620
|
const output = new CaptureOutput();
|
|
526
621
|
const httpCalls: string[] = [];
|
|
@@ -571,7 +666,7 @@ describe("cli agent run client", () => {
|
|
|
571
666
|
|
|
572
667
|
const result = await runAgentTurn({
|
|
573
668
|
agentName: "pm",
|
|
574
|
-
agentKey: "
|
|
669
|
+
agentKey: "agent-custom-platform-tools",
|
|
575
670
|
serverUrl: "https://nolo.chat",
|
|
576
671
|
message: "write task rows",
|
|
577
672
|
scriptDir: "C:/missing/scripts",
|
|
@@ -608,13 +703,112 @@ describe("cli agent run client", () => {
|
|
|
608
703
|
|
|
609
704
|
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server" });
|
|
610
705
|
expect(httpCalls).toHaveLength(1);
|
|
611
|
-
expect(httpCalls[0]?.body.agentKey).toBe("
|
|
706
|
+
expect(httpCalls[0]?.body.agentKey).toBe("agent-custom-platform-tools");
|
|
612
707
|
expect(output.text()).toContain("auto runtime: skipping local runtime");
|
|
613
708
|
expect(output.text()).toContain("queryTableRows, addTableRow, updateTableRow");
|
|
614
709
|
expect(output.text()).toContain("pm -> working");
|
|
615
710
|
expect(output.text()).toContain("pm > server ok");
|
|
616
711
|
});
|
|
617
712
|
|
|
713
|
+
test("auto mode skips local runtime for server platform tools declared by runtime policy", async () => {
|
|
714
|
+
const output = new CaptureOutput();
|
|
715
|
+
const httpCalls: Array<{ url: string; body: any }> = [];
|
|
716
|
+
|
|
717
|
+
const result = await runAgentTurn({
|
|
718
|
+
agentName: "pm",
|
|
719
|
+
agentKey: "agent-policy-platform-tools",
|
|
720
|
+
serverUrl: "https://nolo.chat",
|
|
721
|
+
message: "query task rows",
|
|
722
|
+
scriptDir: "C:/missing/scripts",
|
|
723
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
724
|
+
output,
|
|
725
|
+
runtimeMode: "auto",
|
|
726
|
+
localRuntimeAdapter: {
|
|
727
|
+
host: "cli",
|
|
728
|
+
capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
|
|
729
|
+
loadAgentConfig: async (agentRef) => ({
|
|
730
|
+
key: agentRef,
|
|
731
|
+
name: "PM",
|
|
732
|
+
prompt: "Manage task rows",
|
|
733
|
+
model: "fake-local",
|
|
734
|
+
runtimeToolPolicy: {
|
|
735
|
+
version: 1,
|
|
736
|
+
agentTools: ["queryTableRows", "taskRun"],
|
|
737
|
+
runtimeTools: ["execShell"],
|
|
738
|
+
},
|
|
739
|
+
}),
|
|
740
|
+
loadDialogHistory: async () => [],
|
|
741
|
+
saveTurn: async () => {
|
|
742
|
+
throw new Error("local runtime should be skipped");
|
|
743
|
+
},
|
|
744
|
+
resolveProvider: async () => {
|
|
745
|
+
throw new Error("local provider should be skipped");
|
|
746
|
+
},
|
|
747
|
+
executeTool: async () => {
|
|
748
|
+
throw new Error("local tools should be skipped");
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
scriptPathExists: () => false,
|
|
752
|
+
fetchImpl: async (url, init) => {
|
|
753
|
+
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
754
|
+
return Response.json({ content: "server ok", dialogId: "dialog-server-policy" });
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server-policy" });
|
|
759
|
+
expect(httpCalls).toHaveLength(1);
|
|
760
|
+
expect(httpCalls[0]?.body.agentKey).toBe("agent-policy-platform-tools");
|
|
761
|
+
expect(output.text()).toContain("auto runtime: skipping local runtime");
|
|
762
|
+
expect(output.text()).toContain("queryTableRows, taskRun");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("auto mode skips known platform agents when local config cannot be read", async () => {
|
|
766
|
+
const output = new CaptureOutput();
|
|
767
|
+
const httpCalls: Array<{ url: string; body: any }> = [];
|
|
768
|
+
|
|
769
|
+
const result = await runAgentTurn({
|
|
770
|
+
agentName: "nolo-project-manager",
|
|
771
|
+
agentKey: NOLO_PROJECT_MANAGER_AGENT_KEY,
|
|
772
|
+
serverUrl: "https://us.nolo.chat",
|
|
773
|
+
message: "write task rows",
|
|
774
|
+
scriptDir: "C:/missing/scripts",
|
|
775
|
+
env: { AUTH_TOKEN: "token-123", OPENAI_API_KEY: "local-provider-present" },
|
|
776
|
+
output,
|
|
777
|
+
runtimeMode: "auto",
|
|
778
|
+
localRuntimeAdapter: {
|
|
779
|
+
host: "cli",
|
|
780
|
+
capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
|
|
781
|
+
loadAgentConfig: async () => {
|
|
782
|
+
throw new Error("Database failed to open: LOCK");
|
|
783
|
+
},
|
|
784
|
+
loadDialogHistory: async () => {
|
|
785
|
+
throw new Error("local runtime should be skipped");
|
|
786
|
+
},
|
|
787
|
+
saveTurn: async () => {
|
|
788
|
+
throw new Error("local runtime should be skipped");
|
|
789
|
+
},
|
|
790
|
+
resolveProvider: async () => {
|
|
791
|
+
throw new Error("local provider should be skipped");
|
|
792
|
+
},
|
|
793
|
+
executeTool: async () => {
|
|
794
|
+
throw new Error("local tools should be skipped");
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
scriptPathExists: () => false,
|
|
798
|
+
fetchImpl: async (url, init) => {
|
|
799
|
+
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
800
|
+
return Response.json({ content: "server ok", dialogId: "dialog-server" });
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server" });
|
|
805
|
+
expect(httpCalls).toHaveLength(1);
|
|
806
|
+
expect(httpCalls[0]?.body.agentKey).toBe(NOLO_PROJECT_MANAGER_AGENT_KEY);
|
|
807
|
+
expect(output.text()).toContain("known platform agent");
|
|
808
|
+
expect(output.text()).toContain("nolo-project-manager -> working");
|
|
809
|
+
expect(output.text()).toContain("nolo-project-manager > server ok");
|
|
810
|
+
});
|
|
811
|
+
|
|
618
812
|
test("builds the default local adapter when env requests local mode", async () => {
|
|
619
813
|
const output = new CaptureOutput();
|
|
620
814
|
const builtModes: string[] = [];
|
|
@@ -685,11 +879,67 @@ describe("cli agent run client", () => {
|
|
|
685
879
|
});
|
|
686
880
|
|
|
687
881
|
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-stream" });
|
|
688
|
-
expect(output.text()).toContain("nolo -> working");
|
|
689
|
-
expect(output.text()).toContain("nolo > 你好");
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
test("
|
|
882
|
+
expect(output.text()).toContain("nolo -> working");
|
|
883
|
+
expect(output.text()).toContain("nolo > 你好");
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test("returns a recoverable dialog when a server stream drops after dialog creation", async () => {
|
|
887
|
+
const output = new CaptureOutput();
|
|
888
|
+
|
|
889
|
+
const result = await runAgentTurn({
|
|
890
|
+
agentName: "nolo",
|
|
891
|
+
agentKey: "agent-pub-test",
|
|
892
|
+
serverUrl: "https://nolo.chat",
|
|
893
|
+
message: "hello",
|
|
894
|
+
scriptDir: "C:/missing/scripts",
|
|
895
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
896
|
+
output,
|
|
897
|
+
scriptPathExists: () => false,
|
|
898
|
+
fetchImpl: async () => {
|
|
899
|
+
let sent = false;
|
|
900
|
+
return new Response(
|
|
901
|
+
new ReadableStream({
|
|
902
|
+
pull(controller) {
|
|
903
|
+
const encoder = new TextEncoder();
|
|
904
|
+
if (!sent) {
|
|
905
|
+
sent = true;
|
|
906
|
+
controller.enqueue(
|
|
907
|
+
encoder.encode(
|
|
908
|
+
[
|
|
909
|
+
`data: ${JSON.stringify({
|
|
910
|
+
type: "dialog",
|
|
911
|
+
dialogId: "dialog-recoverable",
|
|
912
|
+
status: "running",
|
|
913
|
+
})}`,
|
|
914
|
+
"",
|
|
915
|
+
`data: ${JSON.stringify({ type: "text", content: "partial" })}`,
|
|
916
|
+
"",
|
|
917
|
+
].join("\n")
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
controller.error(new Error("socket closed"));
|
|
923
|
+
},
|
|
924
|
+
}),
|
|
925
|
+
{
|
|
926
|
+
status: 200,
|
|
927
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
928
|
+
}
|
|
929
|
+
);
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
expect(result).toEqual({
|
|
934
|
+
exitCode: 0,
|
|
935
|
+
dialogId: "dialog-recoverable",
|
|
936
|
+
streamInterrupted: true,
|
|
937
|
+
});
|
|
938
|
+
expect(output.text()).toContain("dialog dialog-recoverable was created");
|
|
939
|
+
expect(output.text()).toContain("read the dialog before retrying");
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("prints an auth hint when installed without repo scripts or AUTH_TOKEN", async () => {
|
|
693
943
|
const output = new CaptureOutput();
|
|
694
944
|
|
|
695
945
|
const result = await runAgentTurn({
|