nolo-cli 0.1.18 → 0.1.20

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.
Files changed (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +14 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +78 -6
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +2 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +289 -54
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +823 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +361 -7
  79. package/client/agentRun.ts +449 -63
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +813 -20
  89. package/client/localRuntimeAdapter.ts +279 -232
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +237 -0
@@ -0,0 +1,823 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export const TASK_RUN_SCHEMA_VERSION = 1;
4
+
5
+ export const TASK_RUN_TOOL_ACTIONS = [
6
+ "readContext",
7
+ "readTaskContext",
8
+ "queryReadyWorkItems",
9
+ "updateWorkItems",
10
+ "upsertWorkItem",
11
+ "claimOrDispatch",
12
+ "claimWorkItem",
13
+ "openDialog",
14
+ "recordArtifact",
15
+ "markBlocked",
16
+ "setBlocker",
17
+ "clearBlocker",
18
+ "requestReview",
19
+ "recordReview",
20
+ "submitOutcome",
21
+ "completeWorkItem",
22
+ "closeWorkItem",
23
+ ] as const;
24
+
25
+ export type TaskRunToolAction = (typeof TASK_RUN_TOOL_ACTIONS)[number];
26
+
27
+ export type TaskRunPhase =
28
+ | "planned"
29
+ | "running"
30
+ | "blocked"
31
+ | "needs_review"
32
+ | "needs_changes"
33
+ | "ready_for_alpha"
34
+ | "alpha_deployed"
35
+ | "main_approval_required"
36
+ | "main_ready"
37
+ | "ready_to_commit"
38
+ | "completed";
39
+
40
+ export type TaskRunWorkItem = {
41
+ id: string;
42
+ kind?: "work" | "review" | "coordination" | string;
43
+ title: string;
44
+ ownerAgent?: string;
45
+ status:
46
+ | "pending"
47
+ | "ready"
48
+ | "running"
49
+ | "blocked"
50
+ | "needs_review"
51
+ | "needs_changes"
52
+ | "completed";
53
+ dependsOn?: string[];
54
+ parallelGroup?: string;
55
+ writeScope?: string[];
56
+ reviewPolicy?: "none" | "after_item" | "after_group" | "before_commit";
57
+ reviewOfWorkItemId?: string;
58
+ dialogIds?: string[];
59
+ artifactRefs?: string[];
60
+ blockerIds?: string[];
61
+ outcome?: {
62
+ summary: string;
63
+ submittedAt: string;
64
+ artifactRefs?: string[];
65
+ closedAt?: string;
66
+ closeSummary?: string;
67
+ closeReason?: string;
68
+ };
69
+ };
70
+
71
+ export type TaskRunAgentRun = {
72
+ id: string;
73
+ agentKey: string;
74
+ role?: string;
75
+ workItemId?: string;
76
+ dialogId?: string;
77
+ status: "queued" | "running" | "completed" | "failed" | "timed_out";
78
+ startedAt: string;
79
+ updatedAt: string;
80
+ resultSummary?: string;
81
+ errorCode?: string;
82
+ };
83
+
84
+ export type TaskRunArtifact = {
85
+ id: string;
86
+ kind: "diff" | "patch" | "commit" | "test" | "screenshot" | "log" | "handoff" | string;
87
+ ref: string;
88
+ summary: string;
89
+ createdAt: string;
90
+ producerRunId?: string;
91
+ workItemId?: string;
92
+ };
93
+
94
+ export type TaskRunDialog = {
95
+ id: string;
96
+ purpose: "assignment" | "review" | "status" | "question" | "handoff" | string;
97
+ participantAgentKeys: string[];
98
+ workItemIds?: string[];
99
+ message?: string;
100
+ status: "open" | "closed";
101
+ openedAt: string;
102
+ closedAt?: string;
103
+ };
104
+
105
+ export type TaskRunEvent = {
106
+ id: string;
107
+ type: string;
108
+ actorId?: string;
109
+ agentKey?: string;
110
+ workItemId?: string;
111
+ dialogId?: string;
112
+ artifactId?: string;
113
+ reviewStatus?: TaskRunReview["status"];
114
+ reason?: string;
115
+ payload?: Record<string, unknown>;
116
+ createdAt: string;
117
+ };
118
+
119
+ export type TaskRunBlocker = {
120
+ id: string;
121
+ layer: string;
122
+ code: string;
123
+ message: string;
124
+ createdAt: string;
125
+ evidence?: string[];
126
+ resolvedAt?: string;
127
+ };
128
+
129
+ export type TaskRunReviewFinding = {
130
+ priority: "P0" | "P1" | "P2" | "P3";
131
+ summary: string;
132
+ file?: string;
133
+ line?: number;
134
+ workItemId?: string;
135
+ };
136
+
137
+ export type TaskRunReview = {
138
+ status: "not_requested" | "requested" | "passed" | "needs_changes" | "blocked";
139
+ reviewerAgentKey?: string;
140
+ targetWorkItemId?: string;
141
+ reviewWorkItemId?: string;
142
+ dialogId?: string;
143
+ reviewedArtifactIds?: string[];
144
+ findings?: TaskRunReviewFinding[];
145
+ updatedAt: string;
146
+ };
147
+
148
+ export type TaskRunAutonomy = "manual" | "supervised" | "auto24h";
149
+
150
+ export type TaskRunBudgetRule = {
151
+ provider?: string;
152
+ model?: string;
153
+ freeUntil?: string;
154
+ dailyUsdLimit?: number;
155
+ hardStopAfter?: string;
156
+ note?: string;
157
+ };
158
+
159
+ export type TaskRunCodexPolicy = {
160
+ mode: "quality_gate_only" | "supervisor" | "allowed";
161
+ requireFor?: string[];
162
+ avoidFor?: string[];
163
+ note?: string;
164
+ };
165
+
166
+ export type TaskRunCodexUsageSnapshot = {
167
+ source: "local_codex_session";
168
+ observedAt?: string;
169
+ planType?: string;
170
+ primary?: {
171
+ usedPercent?: number;
172
+ windowMinutes?: number;
173
+ resetsAt?: number;
174
+ };
175
+ secondary?: {
176
+ usedPercent?: number;
177
+ windowMinutes?: number;
178
+ resetsAt?: number;
179
+ };
180
+ lastTotalTokens?: number;
181
+ totalTokens?: number;
182
+ };
183
+
184
+ export type TaskRunControlPolicy = {
185
+ autonomy?: TaskRunAutonomy;
186
+ nextWakeAt?: string;
187
+ pausedReason?: string;
188
+ budget?: {
189
+ dailyUsdLimit?: number;
190
+ modelBudget?: Record<string, TaskRunBudgetRule>;
191
+ runnerBudget?: Record<string, TaskRunBudgetRule>;
192
+ };
193
+ codexPolicy?: TaskRunCodexPolicy;
194
+ codexUsageSnapshot?: TaskRunCodexUsageSnapshot;
195
+ };
196
+
197
+ export type TaskRunContext = {
198
+ schemaVersion: typeof TASK_RUN_SCHEMA_VERSION;
199
+ taskRunId: string;
200
+ taskRowId: string;
201
+ objective: string;
202
+ phase: TaskRunPhase;
203
+ control?: TaskRunControlPolicy;
204
+ updatedAt: string;
205
+ };
206
+
207
+ export type TaskRunMeta = {
208
+ taskRun?: TaskRunContext;
209
+ workItems?: TaskRunWorkItem[];
210
+ agentRuns?: TaskRunAgentRun[];
211
+ artifacts?: TaskRunArtifact[];
212
+ dialogs?: TaskRunDialog[];
213
+ taskEvents?: TaskRunEvent[];
214
+ review?: TaskRunReview;
215
+ blockers?: TaskRunBlocker[];
216
+ [key: string]: unknown;
217
+ };
218
+
219
+ export type TaskRunRowLike = {
220
+ rowId: string;
221
+ title?: string;
222
+ meta?: TaskRunMeta;
223
+ };
224
+
225
+ export type TaskRunDispatchPlan = {
226
+ ready: TaskRunWorkItem[];
227
+ waiting: Array<TaskRunWorkItem & { waitingOn: string[] }>;
228
+ active: TaskRunWorkItem[];
229
+ completed: TaskRunWorkItem[];
230
+ };
231
+
232
+ const createProtocolId = (): string => randomUUID().replace(/-/g, "");
233
+
234
+ const unique = (values: Array<string | undefined>): string[] => [
235
+ ...new Set(values.filter((value): value is string => Boolean(value))),
236
+ ];
237
+
238
+ export const DEFAULT_TASK_RUN_CONTROL_POLICY: TaskRunControlPolicy = {
239
+ autonomy: "supervised",
240
+ budget: {
241
+ modelBudget: {
242
+ projectManager: {
243
+ provider: "custom",
244
+ model: "kimi-k2.5",
245
+ note: "Default low-cost PM routing.",
246
+ },
247
+ frontend: {
248
+ model: "kimi-2.6",
249
+ freeUntil: "2026-05-22",
250
+ dailyUsdLimit: 1,
251
+ note: "Frontend implementation should prefer Kimi 2.6 while the free window is active.",
252
+ },
253
+ mimoCustomKey: {
254
+ provider: "mimo",
255
+ freeUntil: "2026-05-31",
256
+ note: "Free-use window before normal budget enforcement.",
257
+ },
258
+ },
259
+ },
260
+ codexPolicy: {
261
+ mode: "quality_gate_only",
262
+ requireFor: ["supervision", "critical_review", "complex_unblock", "main_or_release_decision"],
263
+ avoidFor: ["routine_implementation", "first_pass_triage", "formatting", "low_risk_probe"],
264
+ note: "Codex membership usage is still window-limited, so spend it on supervision and quality gates.",
265
+ },
266
+ };
267
+
268
+ const cloneTaskRunControlPolicy = (policy: TaskRunControlPolicy): TaskRunControlPolicy =>
269
+ JSON.parse(JSON.stringify(policy));
270
+
271
+ const cloneMeta = (meta: TaskRunMeta | undefined): TaskRunMeta => ({ ...(meta ?? {}) });
272
+
273
+ const touchTaskRun = (meta: TaskRunMeta, now: string, phase?: TaskRunPhase): void => {
274
+ if (!meta.taskRun) return;
275
+ meta.taskRun = {
276
+ ...meta.taskRun,
277
+ ...(phase ? { phase } : {}),
278
+ updatedAt: now,
279
+ };
280
+ };
281
+
282
+ const phaseForWorkItemStatus = (status: TaskRunWorkItem["status"]): TaskRunPhase | undefined => {
283
+ if (status === "running") return "running";
284
+ if (status === "blocked") return "blocked";
285
+ if (status === "needs_review") return "needs_review";
286
+ if (status === "needs_changes") return "needs_changes";
287
+ return undefined;
288
+ };
289
+
290
+ const appendTaskRunEvent = (args: {
291
+ meta: TaskRunMeta;
292
+ type: string;
293
+ now: string;
294
+ createId?: () => string;
295
+ actorId?: string;
296
+ agentKey?: string;
297
+ workItemId?: string;
298
+ dialogId?: string;
299
+ artifactId?: string;
300
+ reviewStatus?: TaskRunReview["status"];
301
+ reason?: string;
302
+ payload?: Record<string, unknown>;
303
+ }): TaskRunEvent => {
304
+ const event: TaskRunEvent = {
305
+ id: args.createId?.() ?? createProtocolId(),
306
+ type: args.type,
307
+ ...(args.actorId ? { actorId: args.actorId } : {}),
308
+ ...(args.agentKey ? { agentKey: args.agentKey } : {}),
309
+ ...(args.workItemId ? { workItemId: args.workItemId } : {}),
310
+ ...(args.dialogId ? { dialogId: args.dialogId } : {}),
311
+ ...(args.artifactId ? { artifactId: args.artifactId } : {}),
312
+ ...(args.reviewStatus ? { reviewStatus: args.reviewStatus } : {}),
313
+ ...(args.reason ? { reason: args.reason } : {}),
314
+ ...(args.payload ? { payload: args.payload } : {}),
315
+ createdAt: args.now,
316
+ };
317
+ args.meta.taskEvents = [...(args.meta.taskEvents ?? []), event];
318
+ return event;
319
+ };
320
+
321
+ export function prepareTaskRunMeta(args: {
322
+ row: TaskRunRowLike;
323
+ now?: string;
324
+ createId?: () => string;
325
+ }): { meta: TaskRunMeta; taskRun: TaskRunContext } {
326
+ const now = args.now ?? new Date().toISOString();
327
+ const meta = cloneMeta(args.row.meta);
328
+ const existing = meta.taskRun;
329
+ const { allowedActions: _legacyAllowedActions, ...existingTaskRun } = existing ?? {};
330
+ const taskRun: TaskRunContext = {
331
+ ...existingTaskRun,
332
+ schemaVersion: TASK_RUN_SCHEMA_VERSION,
333
+ taskRunId: existing?.taskRunId ?? `taskrun-${args.row.rowId}-${args.createId?.() ?? createProtocolId()}`,
334
+ taskRowId: existing?.taskRowId ?? args.row.rowId,
335
+ objective: existing?.objective ?? args.row.title ?? args.row.rowId,
336
+ phase: existing?.phase ?? "planned",
337
+ control: existing?.control ?? cloneTaskRunControlPolicy(DEFAULT_TASK_RUN_CONTROL_POLICY),
338
+ updatedAt: now,
339
+ };
340
+ meta.taskRun = taskRun;
341
+ return { meta, taskRun };
342
+ }
343
+
344
+ export function planTaskRunDispatch(meta: TaskRunMeta): TaskRunDispatchPlan {
345
+ const workItems = meta.workItems ?? [];
346
+ const completedIds = new Set(workItems.filter((item) => item.status === "completed").map((item) => item.id));
347
+ const ready: TaskRunWorkItem[] = [];
348
+ const waiting: Array<TaskRunWorkItem & { waitingOn: string[] }> = [];
349
+ const active: TaskRunWorkItem[] = [];
350
+ const completed: TaskRunWorkItem[] = [];
351
+
352
+ for (const item of workItems) {
353
+ if (item.status === "completed") {
354
+ completed.push(item);
355
+ continue;
356
+ }
357
+ if (item.status === "running" || item.status === "needs_review") {
358
+ active.push(item);
359
+ continue;
360
+ }
361
+ if (item.status === "blocked") {
362
+ waiting.push({ ...item, waitingOn: item.blockerIds ?? [] });
363
+ continue;
364
+ }
365
+ if (item.status === "needs_changes") {
366
+ ready.push(item);
367
+ continue;
368
+ }
369
+ const missingDeps = (item.dependsOn ?? []).filter((depId) => !completedIds.has(depId));
370
+ if (missingDeps.length > 0) {
371
+ waiting.push({ ...item, waitingOn: missingDeps });
372
+ continue;
373
+ }
374
+ ready.push(item);
375
+ }
376
+
377
+ return { ready, waiting, active, completed };
378
+ }
379
+
380
+ export function upsertTaskRunWorkItems(args: {
381
+ meta: TaskRunMeta;
382
+ workItems: TaskRunWorkItem[];
383
+ now?: string;
384
+ }): TaskRunMeta {
385
+ const now = args.now ?? new Date().toISOString();
386
+ const meta = cloneMeta(args.meta);
387
+ const items = [...(meta.workItems ?? [])];
388
+ for (const input of args.workItems) {
389
+ const index = items.findIndex((item) => item.id === input.id);
390
+ const existing = index >= 0 ? items[index] : undefined;
391
+ const workItem = existing
392
+ ? {
393
+ ...existing,
394
+ ...input,
395
+ dependsOn: unique([...(existing.dependsOn ?? []), ...(input.dependsOn ?? [])]),
396
+ writeScope: unique([...(existing.writeScope ?? []), ...(input.writeScope ?? [])]),
397
+ dialogIds: unique([...(existing.dialogIds ?? []), ...(input.dialogIds ?? [])]),
398
+ artifactRefs: unique([...(existing.artifactRefs ?? []), ...(input.artifactRefs ?? [])]),
399
+ blockerIds: unique([...(existing.blockerIds ?? []), ...(input.blockerIds ?? [])]),
400
+ }
401
+ : input;
402
+ if (index >= 0) {
403
+ items[index] = workItem;
404
+ } else {
405
+ items.push(workItem);
406
+ }
407
+ appendTaskRunEvent({
408
+ meta,
409
+ type: index >= 0 ? "work_item.updated" : "work_item.created",
410
+ workItemId: workItem.id,
411
+ now,
412
+ payload: { status: workItem.status, ownerAgent: workItem.ownerAgent },
413
+ });
414
+ touchTaskRun(meta, now, phaseForWorkItemStatus(workItem.status));
415
+ }
416
+ meta.workItems = items;
417
+ return meta;
418
+ }
419
+
420
+ export function updateTaskRunWorkItem(args: {
421
+ meta: TaskRunMeta;
422
+ id: string;
423
+ status?: TaskRunWorkItem["status"];
424
+ dialogIds?: string[];
425
+ artifactRefs?: string[];
426
+ blockerIds?: string[];
427
+ now?: string;
428
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem } {
429
+ const now = args.now ?? new Date().toISOString();
430
+ const meta = cloneMeta(args.meta);
431
+ const items = [...(meta.workItems ?? [])];
432
+ const index = items.findIndex((item) => item.id === args.id);
433
+ if (index < 0) return { meta };
434
+ const existing = items[index];
435
+ const workItem = {
436
+ ...existing,
437
+ ...(args.status ? { status: args.status } : {}),
438
+ dialogIds: unique([...(existing.dialogIds ?? []), ...(args.dialogIds ?? [])]),
439
+ artifactRefs: unique([...(existing.artifactRefs ?? []), ...(args.artifactRefs ?? [])]),
440
+ blockerIds: unique([...(existing.blockerIds ?? []), ...(args.blockerIds ?? [])]),
441
+ };
442
+ items[index] = workItem;
443
+ meta.workItems = items;
444
+ appendTaskRunEvent({
445
+ meta,
446
+ type: args.status ? "work_item.status_changed" : "work_item.updated",
447
+ workItemId: workItem.id,
448
+ now,
449
+ payload: {
450
+ fromStatus: existing.status,
451
+ toStatus: workItem.status,
452
+ artifactRefs: args.artifactRefs,
453
+ blockerIds: args.blockerIds,
454
+ dialogIds: args.dialogIds,
455
+ },
456
+ });
457
+ touchTaskRun(meta, now, args.status ? phaseForWorkItemStatus(args.status) : undefined);
458
+ return { meta, workItem };
459
+ }
460
+
461
+ export function recordTaskRunDialog(args: {
462
+ meta: TaskRunMeta;
463
+ purpose: TaskRunDialog["purpose"];
464
+ participantAgentKeys: string[];
465
+ workItemIds?: string[];
466
+ message?: string;
467
+ now?: string;
468
+ createId?: () => string;
469
+ }): { meta: TaskRunMeta; dialog: TaskRunDialog } {
470
+ const now = args.now ?? new Date().toISOString();
471
+ const meta = cloneMeta(args.meta);
472
+ const dialog: TaskRunDialog = {
473
+ id: args.createId?.() ?? createProtocolId(),
474
+ purpose: args.purpose,
475
+ participantAgentKeys: unique(args.participantAgentKeys),
476
+ ...(args.workItemIds?.length ? { workItemIds: unique(args.workItemIds) } : {}),
477
+ ...(args.message ? { message: args.message } : {}),
478
+ status: "open",
479
+ openedAt: now,
480
+ };
481
+ meta.dialogs = [...(meta.dialogs ?? []), dialog];
482
+ appendTaskRunEvent({
483
+ meta,
484
+ type: "dialog.opened",
485
+ dialogId: dialog.id,
486
+ now,
487
+ payload: {
488
+ purpose: dialog.purpose,
489
+ participantAgentKeys: dialog.participantAgentKeys,
490
+ workItemIds: dialog.workItemIds,
491
+ },
492
+ });
493
+ let nextMeta = meta;
494
+ for (const workItemId of dialog.workItemIds ?? []) {
495
+ nextMeta = updateTaskRunWorkItem({ meta: nextMeta, id: workItemId, dialogIds: [dialog.id], now }).meta;
496
+ }
497
+ touchTaskRun(nextMeta, now);
498
+ return { meta: nextMeta, dialog };
499
+ }
500
+
501
+ export function recordTaskRunArtifact(args: {
502
+ meta: TaskRunMeta;
503
+ artifact: Omit<TaskRunArtifact, "id" | "createdAt">;
504
+ now?: string;
505
+ createId?: () => string;
506
+ }): { meta: TaskRunMeta; artifact: TaskRunArtifact } {
507
+ const now = args.now ?? new Date().toISOString();
508
+ const meta = cloneMeta(args.meta);
509
+ const artifact = { id: args.createId?.() ?? createProtocolId(), ...args.artifact, createdAt: now };
510
+ meta.artifacts = [...(meta.artifacts ?? []), artifact];
511
+ appendTaskRunEvent({
512
+ meta,
513
+ type: "artifact.recorded",
514
+ artifactId: artifact.id,
515
+ workItemId: artifact.workItemId,
516
+ now,
517
+ payload: { kind: artifact.kind, ref: artifact.ref, summary: artifact.summary },
518
+ });
519
+ touchTaskRun(meta, now);
520
+ return { meta, artifact };
521
+ }
522
+
523
+ export function recordTaskRunBlocker(args: {
524
+ meta: TaskRunMeta;
525
+ blocker: Omit<TaskRunBlocker, "id" | "createdAt">;
526
+ now?: string;
527
+ createId?: () => string;
528
+ }): { meta: TaskRunMeta; blocker: TaskRunBlocker } {
529
+ const now = args.now ?? new Date().toISOString();
530
+ const meta = cloneMeta(args.meta);
531
+ const blocker = { id: args.createId?.() ?? createProtocolId(), ...args.blocker, createdAt: now };
532
+ meta.blockers = [...(meta.blockers ?? []), blocker];
533
+ appendTaskRunEvent({
534
+ meta,
535
+ type: "blocker.recorded",
536
+ now,
537
+ payload: { blockerId: blocker.id, layer: blocker.layer, code: blocker.code, message: blocker.message },
538
+ });
539
+ touchTaskRun(meta, now, "blocked");
540
+ return { meta, blocker };
541
+ }
542
+
543
+ export function clearTaskRunBlocker(args: {
544
+ meta: TaskRunMeta;
545
+ blockerId: string;
546
+ workItemId?: string;
547
+ now?: string;
548
+ }): { meta: TaskRunMeta; blocker?: TaskRunBlocker; workItem?: TaskRunWorkItem } {
549
+ const now = args.now ?? new Date().toISOString();
550
+ const meta = cloneMeta(args.meta);
551
+ const blockers = [...(meta.blockers ?? [])];
552
+ const blockerIndex = blockers.findIndex((blocker) => blocker.id === args.blockerId);
553
+ if (blockerIndex < 0) return { meta };
554
+ const blocker = { ...blockers[blockerIndex], resolvedAt: now };
555
+ blockers[blockerIndex] = blocker;
556
+ meta.blockers = blockers;
557
+ let workItem: TaskRunWorkItem | undefined;
558
+ if (args.workItemId) {
559
+ const items = [...(meta.workItems ?? [])];
560
+ const itemIndex = items.findIndex((item) => item.id === args.workItemId);
561
+ if (itemIndex >= 0) {
562
+ const existing = items[itemIndex];
563
+ const blockerIds = (existing.blockerIds ?? []).filter((id) => id !== args.blockerId);
564
+ workItem = {
565
+ ...existing,
566
+ blockerIds,
567
+ ...(existing.status === "blocked" && blockerIds.length === 0 ? { status: "pending" as const } : {}),
568
+ };
569
+ items[itemIndex] = workItem;
570
+ meta.workItems = items;
571
+ }
572
+ }
573
+ appendTaskRunEvent({
574
+ meta,
575
+ type: "blocker.cleared",
576
+ workItemId: args.workItemId,
577
+ now,
578
+ payload: { blockerId: blocker.id },
579
+ });
580
+ touchTaskRun(meta, now);
581
+ return { meta, blocker, workItem };
582
+ }
583
+
584
+ export function recordTaskRunAgentDispatch(args: {
585
+ meta: TaskRunMeta;
586
+ workItemId: string;
587
+ agentKey: string;
588
+ role?: string;
589
+ now?: string;
590
+ createId?: () => string;
591
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem; agentRun?: TaskRunAgentRun } {
592
+ const now = args.now ?? new Date().toISOString();
593
+ const updated = updateTaskRunWorkItem({ meta: args.meta, id: args.workItemId, status: "running", now });
594
+ if (!updated.workItem) return { meta: updated.meta };
595
+ const agentRun: TaskRunAgentRun = {
596
+ id: args.createId?.() ?? createProtocolId(),
597
+ agentKey: args.agentKey,
598
+ role: args.role,
599
+ workItemId: args.workItemId,
600
+ status: "queued",
601
+ startedAt: now,
602
+ updatedAt: now,
603
+ };
604
+ updated.meta.agentRuns = [...(updated.meta.agentRuns ?? []), agentRun];
605
+ appendTaskRunEvent({
606
+ meta: updated.meta,
607
+ type: "work_item.claimed",
608
+ agentKey: args.agentKey,
609
+ workItemId: args.workItemId,
610
+ now,
611
+ payload: { agentRunId: agentRun.id, role: args.role },
612
+ });
613
+ touchTaskRun(updated.meta, now, "running");
614
+ return { meta: updated.meta, workItem: updated.workItem, agentRun };
615
+ }
616
+
617
+ export function submitTaskRunOutcome(args: {
618
+ meta: TaskRunMeta;
619
+ workItemId: string;
620
+ summary: string;
621
+ artifactIds?: string[];
622
+ dialogId?: string;
623
+ now?: string;
624
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem } {
625
+ const now = args.now ?? new Date().toISOString();
626
+ const updated = updateTaskRunWorkItem({
627
+ meta: args.meta,
628
+ id: args.workItemId,
629
+ status: "needs_review",
630
+ dialogIds: args.dialogId ? [args.dialogId] : undefined,
631
+ artifactRefs: args.artifactIds,
632
+ now,
633
+ });
634
+ if (!updated.workItem) return updated;
635
+ const items = [...(updated.meta.workItems ?? [])];
636
+ const index = items.findIndex((item) => item.id === args.workItemId);
637
+ const workItem = {
638
+ ...updated.workItem,
639
+ outcome: {
640
+ summary: args.summary,
641
+ submittedAt: now,
642
+ ...(args.artifactIds?.length ? { artifactRefs: unique(args.artifactIds) } : {}),
643
+ },
644
+ };
645
+ if (index >= 0) items[index] = workItem;
646
+ updated.meta.workItems = items;
647
+ appendTaskRunEvent({
648
+ meta: updated.meta,
649
+ type: "work_item.outcome_submitted",
650
+ workItemId: args.workItemId,
651
+ dialogId: args.dialogId,
652
+ now,
653
+ payload: { summary: args.summary, artifactIds: args.artifactIds },
654
+ });
655
+ touchTaskRun(updated.meta, now, "needs_review");
656
+ return { meta: updated.meta, workItem };
657
+ }
658
+
659
+ export function closeTaskRunWorkItem(args: {
660
+ meta: TaskRunMeta;
661
+ workItemId: string;
662
+ summary: string;
663
+ reason?: string;
664
+ artifactIds?: string[];
665
+ dialogId?: string;
666
+ now?: string;
667
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem } {
668
+ const now = args.now ?? new Date().toISOString();
669
+ const updated = updateTaskRunWorkItem({
670
+ meta: args.meta,
671
+ id: args.workItemId,
672
+ status: "completed",
673
+ dialogIds: args.dialogId ? [args.dialogId] : undefined,
674
+ artifactRefs: args.artifactIds,
675
+ now,
676
+ });
677
+ if (!updated.workItem) return updated;
678
+ const items = [...(updated.meta.workItems ?? [])];
679
+ const index = items.findIndex((item) => item.id === args.workItemId);
680
+ const workItem = {
681
+ ...updated.workItem,
682
+ outcome: {
683
+ ...(updated.workItem.outcome ?? { summary: args.summary, submittedAt: now }),
684
+ closedAt: now,
685
+ closeSummary: args.summary,
686
+ ...(args.reason ? { closeReason: args.reason } : {}),
687
+ ...(args.artifactIds?.length
688
+ ? { artifactRefs: unique([...(updated.workItem.outcome?.artifactRefs ?? []), ...args.artifactIds]) }
689
+ : {}),
690
+ },
691
+ };
692
+ if (index >= 0) items[index] = workItem;
693
+ updated.meta.workItems = items;
694
+ appendTaskRunEvent({
695
+ meta: updated.meta,
696
+ type: "work_item.closed",
697
+ workItemId: args.workItemId,
698
+ dialogId: args.dialogId,
699
+ now,
700
+ reason: args.reason,
701
+ payload: { summary: args.summary, artifactIds: args.artifactIds },
702
+ });
703
+ touchTaskRun(updated.meta, now);
704
+ return { meta: updated.meta, workItem };
705
+ }
706
+
707
+ export function requestTaskRunReview(args: {
708
+ meta: TaskRunMeta;
709
+ reviewerAgentKey: string;
710
+ targetWorkItemId?: string;
711
+ artifactIds?: string[];
712
+ dialogId?: string;
713
+ message?: string;
714
+ now?: string;
715
+ createId?: () => string;
716
+ }): { meta: TaskRunMeta; review: TaskRunReview } {
717
+ const now = args.now ?? new Date().toISOString();
718
+ const meta = cloneMeta(args.meta);
719
+ const reviewWorkItemId = args.targetWorkItemId
720
+ ? `review-${args.targetWorkItemId}-${args.createId?.() ?? createProtocolId()}`
721
+ : undefined;
722
+ let reviewDialogId = args.dialogId;
723
+ let nextMeta = meta;
724
+ if (args.targetWorkItemId) {
725
+ const reviewWorkItem: TaskRunWorkItem = {
726
+ id: reviewWorkItemId ?? `review-${args.targetWorkItemId}`,
727
+ kind: "review",
728
+ title: `Review ${args.targetWorkItemId}`,
729
+ ownerAgent: args.reviewerAgentKey,
730
+ status: "ready",
731
+ dependsOn: [args.targetWorkItemId],
732
+ reviewPolicy: "none",
733
+ reviewOfWorkItemId: args.targetWorkItemId,
734
+ artifactRefs: args.artifactIds,
735
+ };
736
+ nextMeta = upsertTaskRunWorkItems({ meta: nextMeta, workItems: [reviewWorkItem], now });
737
+ if (!reviewDialogId) {
738
+ const opened = recordTaskRunDialog({
739
+ meta: nextMeta,
740
+ purpose: "review",
741
+ participantAgentKeys: [args.reviewerAgentKey],
742
+ workItemIds: [args.targetWorkItemId, reviewWorkItem.id],
743
+ message: args.message ?? `Please review ${args.targetWorkItemId}.`,
744
+ now,
745
+ createId: args.createId,
746
+ });
747
+ nextMeta = opened.meta;
748
+ reviewDialogId = opened.dialog.id;
749
+ }
750
+ }
751
+ const review: TaskRunReview = {
752
+ ...(nextMeta.review ?? { status: "not_requested", updatedAt: now }),
753
+ status: "requested",
754
+ reviewerAgentKey: args.reviewerAgentKey,
755
+ ...(args.targetWorkItemId ? { targetWorkItemId: args.targetWorkItemId } : {}),
756
+ ...(reviewWorkItemId ? { reviewWorkItemId } : {}),
757
+ ...(reviewDialogId ? { dialogId: reviewDialogId } : {}),
758
+ reviewedArtifactIds: unique([...(nextMeta.review?.reviewedArtifactIds ?? []), ...(args.artifactIds ?? [])]),
759
+ updatedAt: now,
760
+ };
761
+ nextMeta.review = review;
762
+ appendTaskRunEvent({
763
+ meta: nextMeta,
764
+ type: "review.requested",
765
+ workItemId: args.targetWorkItemId,
766
+ dialogId: reviewDialogId,
767
+ reviewStatus: "requested",
768
+ agentKey: args.reviewerAgentKey,
769
+ now,
770
+ payload: { artifactIds: args.artifactIds, reviewWorkItemId },
771
+ });
772
+ touchTaskRun(nextMeta, now, "needs_review");
773
+ return { meta: nextMeta, review };
774
+ }
775
+
776
+ export function recordTaskRunReview(args: {
777
+ meta: TaskRunMeta;
778
+ status: Exclude<TaskRunReview["status"], "not_requested" | "requested">;
779
+ targetWorkItemId?: string;
780
+ reviewerAgentKey?: string;
781
+ artifactIds?: string[];
782
+ dialogId?: string;
783
+ findings?: TaskRunReviewFinding[];
784
+ now?: string;
785
+ }): { meta: TaskRunMeta; review: TaskRunReview } {
786
+ const now = args.now ?? new Date().toISOString();
787
+ const meta = cloneMeta(args.meta);
788
+ const review: TaskRunReview = {
789
+ ...(meta.review ?? { status: "not_requested", updatedAt: now }),
790
+ status: args.status,
791
+ ...(args.reviewerAgentKey ? { reviewerAgentKey: args.reviewerAgentKey } : {}),
792
+ ...(args.targetWorkItemId ? { targetWorkItemId: args.targetWorkItemId } : {}),
793
+ ...(args.dialogId ? { dialogId: args.dialogId } : {}),
794
+ reviewedArtifactIds: unique([...(meta.review?.reviewedArtifactIds ?? []), ...(args.artifactIds ?? [])]),
795
+ findings: args.findings ?? meta.review?.findings,
796
+ updatedAt: now,
797
+ };
798
+ meta.review = review;
799
+ let nextMeta = meta;
800
+ const targetWorkItemId = args.targetWorkItemId ?? meta.review?.targetWorkItemId;
801
+ if (args.status === "needs_changes" && targetWorkItemId) {
802
+ nextMeta = updateTaskRunWorkItem({
803
+ meta: nextMeta,
804
+ id: targetWorkItemId,
805
+ status: "needs_changes",
806
+ artifactRefs: args.artifactIds,
807
+ dialogIds: args.dialogId ? [args.dialogId] : undefined,
808
+ now,
809
+ }).meta;
810
+ }
811
+ appendTaskRunEvent({
812
+ meta: nextMeta,
813
+ type: "review.recorded",
814
+ dialogId: args.dialogId,
815
+ workItemId: targetWorkItemId,
816
+ reviewStatus: args.status,
817
+ agentKey: args.reviewerAgentKey,
818
+ now,
819
+ payload: { artifactIds: args.artifactIds, findings: args.findings },
820
+ });
821
+ touchTaskRun(nextMeta, now, args.status === "passed" ? "ready_for_alpha" : args.status);
822
+ return { meta: nextMeta, review };
823
+ }