tinker-agent 1.0.65
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/README.md +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bashCommandFromArgs,
|
|
3
|
+
bashResultDetail,
|
|
4
|
+
type BashDisplayDetail,
|
|
5
|
+
} from "../events/bash-result-detail";
|
|
6
|
+
import type { AgentEvent } from "../events/types";
|
|
7
|
+
import type { ContextUsageSnapshot } from "../agent/context-meter";
|
|
8
|
+
import type {
|
|
9
|
+
ModelContextBudget,
|
|
10
|
+
ModelContextProfile,
|
|
11
|
+
} from "../model/model-context-profile";
|
|
12
|
+
import type { ShellTaskSnapshot, ShellTaskStatus } from "../tools/bash-task";
|
|
13
|
+
import { countPatchChanges } from "../tools/file-diff";
|
|
14
|
+
import type { DiffHunk, ToolRawResult } from "../tools/types";
|
|
15
|
+
import {
|
|
16
|
+
defaultTuiProjectionPolicy,
|
|
17
|
+
type TuiProjectionPolicy,
|
|
18
|
+
validateTuiProjectionPolicy,
|
|
19
|
+
} from "./tui-projection-policy";
|
|
20
|
+
|
|
21
|
+
export type TimelineItem = {
|
|
22
|
+
id: string;
|
|
23
|
+
ref?: string;
|
|
24
|
+
text: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
status: "running" | "ok" | "failed" | "cancelled" | "info" | "text";
|
|
27
|
+
diff?: DiffHunk[];
|
|
28
|
+
diffTruncated?: boolean;
|
|
29
|
+
bash?: BashDisplayDetail;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type TuiTurnProjection = {
|
|
33
|
+
turnId: string;
|
|
34
|
+
turnNumber: number;
|
|
35
|
+
status: "running" | "completed" | "failed" | "cancelled" | "interrupted";
|
|
36
|
+
startedAt: string;
|
|
37
|
+
workedForMs?: number;
|
|
38
|
+
items: TimelineItem[];
|
|
39
|
+
omittedItemCount: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type TuiProjectionState = {
|
|
43
|
+
status: "idle" | "running" | "done" | "failed" | "cancelled";
|
|
44
|
+
sessionId: string;
|
|
45
|
+
modelName: string;
|
|
46
|
+
workspaceRoot: string;
|
|
47
|
+
workedForMs?: number;
|
|
48
|
+
contextProfile?: ModelContextProfile;
|
|
49
|
+
contextBudget?: ModelContextBudget;
|
|
50
|
+
contextUsage?: ContextUsageSnapshot;
|
|
51
|
+
activeTurn?: TuiTurnProjection;
|
|
52
|
+
recentTurns: TuiTurnProjection[];
|
|
53
|
+
notices: TimelineItem[];
|
|
54
|
+
backgroundTasks: ShellTaskSnapshot[];
|
|
55
|
+
omittedTurnCount: number;
|
|
56
|
+
finalText?: string;
|
|
57
|
+
error?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type TuiState = TuiProjectionState;
|
|
61
|
+
|
|
62
|
+
export function createInitialTuiState(input: {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
modelName: string;
|
|
65
|
+
workspaceRoot: string;
|
|
66
|
+
}): TuiProjectionState {
|
|
67
|
+
return {
|
|
68
|
+
status: "idle",
|
|
69
|
+
sessionId: input.sessionId,
|
|
70
|
+
modelName: input.modelName,
|
|
71
|
+
workspaceRoot: input.workspaceRoot,
|
|
72
|
+
recentTurns: [],
|
|
73
|
+
notices: [],
|
|
74
|
+
backgroundTasks: [],
|
|
75
|
+
omittedTurnCount: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const createInitialTuiProjectionState = createInitialTuiState;
|
|
80
|
+
|
|
81
|
+
export function reduceTuiProjection(
|
|
82
|
+
state: TuiProjectionState,
|
|
83
|
+
event: AgentEvent,
|
|
84
|
+
policy: TuiProjectionPolicy = defaultTuiProjectionPolicy,
|
|
85
|
+
): TuiProjectionState {
|
|
86
|
+
validateTuiProjectionPolicy(policy);
|
|
87
|
+
|
|
88
|
+
switch (event.type) {
|
|
89
|
+
case "session.started":
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
sessionId: event.sessionId,
|
|
93
|
+
modelName: event.data.model,
|
|
94
|
+
workspaceRoot: event.data.workspaceRoot,
|
|
95
|
+
contextProfile: event.data.contextProfile,
|
|
96
|
+
contextBudget: event.data.contextBudget,
|
|
97
|
+
};
|
|
98
|
+
case "session.resumed":
|
|
99
|
+
case "session.interrupted_frame_recovered":
|
|
100
|
+
return state;
|
|
101
|
+
case "context.usage.updated":
|
|
102
|
+
return { ...state, contextUsage: event.data.snapshot };
|
|
103
|
+
case "turn.started": {
|
|
104
|
+
if (state.activeTurn !== undefined) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Cannot project turn ${event.turnId} while turn ${state.activeTurn.turnId} is active.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const turnId = requireEventString(event.turnId, "turn.started turnId");
|
|
110
|
+
const turnNumber = requireEventNumber(
|
|
111
|
+
event.turnNumber,
|
|
112
|
+
"turn.started turnNumber",
|
|
113
|
+
);
|
|
114
|
+
parseEventTimestamp(event.timestamp);
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
status: "running",
|
|
118
|
+
workedForMs: undefined,
|
|
119
|
+
finalText: undefined,
|
|
120
|
+
error: undefined,
|
|
121
|
+
activeTurn: {
|
|
122
|
+
turnId,
|
|
123
|
+
turnNumber,
|
|
124
|
+
status: "running",
|
|
125
|
+
startedAt: event.timestamp,
|
|
126
|
+
items: [
|
|
127
|
+
{
|
|
128
|
+
id: `turn-${turnId}-prompt-${event.eventSequence}`,
|
|
129
|
+
label: "prompt",
|
|
130
|
+
text: event.data.userPrompt,
|
|
131
|
+
status: "text",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
omittedItemCount: 0,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
case "model.request.started":
|
|
139
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
140
|
+
appendTurnItem(turn, {
|
|
141
|
+
id: `model-${event.iterationId}`,
|
|
142
|
+
ref: modelRequestRef(event.iterationId),
|
|
143
|
+
text: `model iteration ${event.iterationNumber}`,
|
|
144
|
+
status: "running",
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
case "model.request.finished":
|
|
148
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
149
|
+
updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
150
|
+
...item,
|
|
151
|
+
text: completedModelRequestText(
|
|
152
|
+
requireEventNumber(
|
|
153
|
+
event.iterationNumber,
|
|
154
|
+
"model.request.finished iterationNumber",
|
|
155
|
+
),
|
|
156
|
+
event.data.output.message.toolCalls?.length ?? 0,
|
|
157
|
+
),
|
|
158
|
+
status: "ok",
|
|
159
|
+
})),
|
|
160
|
+
);
|
|
161
|
+
case "assistant.progress":
|
|
162
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
163
|
+
appendTurnItem(turn, {
|
|
164
|
+
id: `assistant-${event.iterationId}-${event.eventSequence}`,
|
|
165
|
+
label: "assistant",
|
|
166
|
+
text: event.data.content,
|
|
167
|
+
status: "text",
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
case "tool.started":
|
|
171
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
172
|
+
appendTurnItem(turn, {
|
|
173
|
+
id: `tool-${event.toolCallId}`,
|
|
174
|
+
ref: toolCallRef(event.data.call.toolCallId),
|
|
175
|
+
status: "running",
|
|
176
|
+
...toolCallStartedProjection(event.data.call),
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
case "tool.raw_result":
|
|
180
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
181
|
+
updateTurnItem(turn, toolCallRef(event.data.call.toolCallId), (item) => ({
|
|
182
|
+
...item,
|
|
183
|
+
...toolRawResultProjection(event.data.call, event.data.raw),
|
|
184
|
+
})),
|
|
185
|
+
);
|
|
186
|
+
case "tool.finished":
|
|
187
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
188
|
+
updateTurnItem(turn, toolCallRef(event.data.call.toolCallId), (item) => ({
|
|
189
|
+
...item,
|
|
190
|
+
status: event.data.ok ? "ok" : "failed",
|
|
191
|
+
})),
|
|
192
|
+
);
|
|
193
|
+
case "bash.task.backgrounded":
|
|
194
|
+
case "bash.task.stopping":
|
|
195
|
+
case "bash.task.finished":
|
|
196
|
+
return {
|
|
197
|
+
...state,
|
|
198
|
+
backgroundTasks: upsertBackgroundTask(
|
|
199
|
+
state.backgroundTasks,
|
|
200
|
+
event.data.task,
|
|
201
|
+
policy.completedTaskLimit,
|
|
202
|
+
),
|
|
203
|
+
};
|
|
204
|
+
case "mcp.server.connected":
|
|
205
|
+
return appendNotice(
|
|
206
|
+
state,
|
|
207
|
+
{
|
|
208
|
+
id: `notice-mcp-${event.eventSequence}`,
|
|
209
|
+
label: "mcp",
|
|
210
|
+
text: `mcp ${event.data.serverName} connected -> ${event.data.toolCount} tool${event.data.toolCount === 1 ? "" : "s"}`,
|
|
211
|
+
status: "info",
|
|
212
|
+
},
|
|
213
|
+
policy.sessionNoticeLimit,
|
|
214
|
+
);
|
|
215
|
+
case "mcp.server.failed":
|
|
216
|
+
return appendNotice(
|
|
217
|
+
state,
|
|
218
|
+
{
|
|
219
|
+
id: `notice-mcp-${event.eventSequence}`,
|
|
220
|
+
label: "mcp",
|
|
221
|
+
text: `mcp ${event.data.serverName} failed -> ${boundedToolError(event.data.error)}`,
|
|
222
|
+
status: "failed",
|
|
223
|
+
},
|
|
224
|
+
policy.sessionNoticeLimit,
|
|
225
|
+
);
|
|
226
|
+
case "diagnostic.sink_failed":
|
|
227
|
+
return appendNotice(
|
|
228
|
+
state,
|
|
229
|
+
{
|
|
230
|
+
id: `notice-sink-${event.eventSequence}`,
|
|
231
|
+
label: "diagnostic",
|
|
232
|
+
text: `event sink ${event.data.sinkName} disabled after ${event.data.failedEventType} failed -> ${boundedToolError(event.data.error)}`,
|
|
233
|
+
status: "failed",
|
|
234
|
+
},
|
|
235
|
+
policy.sessionNoticeLimit,
|
|
236
|
+
);
|
|
237
|
+
case "turn.finished": {
|
|
238
|
+
const active = requireActiveTurn(state, event);
|
|
239
|
+
const withFinal =
|
|
240
|
+
event.data.finalText.trim() === ""
|
|
241
|
+
? active
|
|
242
|
+
: appendTurnItem(active, {
|
|
243
|
+
id: `turn-${active.turnId}-final-${event.eventSequence}`,
|
|
244
|
+
label: "assistant",
|
|
245
|
+
text: event.data.finalText,
|
|
246
|
+
status: "text",
|
|
247
|
+
});
|
|
248
|
+
return finishActiveTurn(
|
|
249
|
+
state,
|
|
250
|
+
event,
|
|
251
|
+
limitTurnItems(withFinal, policy.itemLimitPerTurn),
|
|
252
|
+
"completed",
|
|
253
|
+
policy,
|
|
254
|
+
{
|
|
255
|
+
status: "done",
|
|
256
|
+
finalText: event.data.finalText,
|
|
257
|
+
error: undefined,
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
case "turn.cancelled": {
|
|
262
|
+
const active = requireActiveTurn(state, event);
|
|
263
|
+
const cancelled = applyTurnCancellation(active, event);
|
|
264
|
+
return finishActiveTurn(
|
|
265
|
+
state,
|
|
266
|
+
event,
|
|
267
|
+
limitTurnItems(cancelled, policy.itemLimitPerTurn),
|
|
268
|
+
"cancelled",
|
|
269
|
+
policy,
|
|
270
|
+
{
|
|
271
|
+
status: "cancelled",
|
|
272
|
+
finalText: undefined,
|
|
273
|
+
error: undefined,
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
case "turn.failed": {
|
|
278
|
+
const active = requireActiveTurn(state, event);
|
|
279
|
+
const failed = appendTurnItem(active, {
|
|
280
|
+
id: `turn-${active.turnId}-failed-${event.eventSequence}`,
|
|
281
|
+
label: "error",
|
|
282
|
+
text: event.data.error,
|
|
283
|
+
status: "failed",
|
|
284
|
+
});
|
|
285
|
+
return finishActiveTurn(
|
|
286
|
+
state,
|
|
287
|
+
event,
|
|
288
|
+
limitTurnItems(failed, policy.itemLimitPerTurn),
|
|
289
|
+
"failed",
|
|
290
|
+
policy,
|
|
291
|
+
{
|
|
292
|
+
status: "failed",
|
|
293
|
+
finalText: undefined,
|
|
294
|
+
error: event.data.error,
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
case "session.finished":
|
|
299
|
+
case "agent.iteration.started":
|
|
300
|
+
case "agent.iteration.finished":
|
|
301
|
+
case "context.shadow.planned":
|
|
302
|
+
case "context.shadow.failed":
|
|
303
|
+
case "context.revision.started":
|
|
304
|
+
case "context.revision.finished":
|
|
305
|
+
case "context.revision.failed":
|
|
306
|
+
case "tool.observation":
|
|
307
|
+
return state;
|
|
308
|
+
default:
|
|
309
|
+
return assertNever(event);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export const applyAgentEvent = reduceTuiProjection;
|
|
314
|
+
|
|
315
|
+
export function visibleTimelineItems(state: TuiProjectionState): TimelineItem[] {
|
|
316
|
+
const items = [...state.notices];
|
|
317
|
+
|
|
318
|
+
if (state.omittedTurnCount > 0) {
|
|
319
|
+
items.push({
|
|
320
|
+
id: "projection-omitted-turns",
|
|
321
|
+
label: "live view",
|
|
322
|
+
text: `${state.omittedTurnCount} earlier turn${state.omittedTurnCount === 1 ? "" : "s"} omitted; full diagnostics remain on disk`,
|
|
323
|
+
status: "info",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const turn of [
|
|
328
|
+
...state.recentTurns,
|
|
329
|
+
...(state.activeTurn === undefined ? [] : [state.activeTurn]),
|
|
330
|
+
]) {
|
|
331
|
+
items.push(...visibleTurnItems(turn));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return items;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function visibleTurnItems(turn: TuiTurnProjection): TimelineItem[] {
|
|
338
|
+
if (turn.omittedItemCount === 0) {
|
|
339
|
+
return turn.items;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const marker: TimelineItem = {
|
|
343
|
+
id: `turn-${turn.turnId}-omitted-items`,
|
|
344
|
+
label: "live view",
|
|
345
|
+
text: `${turn.omittedItemCount} earlier timeline item${turn.omittedItemCount === 1 ? "" : "s"} omitted; full diagnostics remain on disk`,
|
|
346
|
+
status: "info",
|
|
347
|
+
};
|
|
348
|
+
const promptIndex = turn.items.findIndex((item) => item.label === "prompt");
|
|
349
|
+
if (promptIndex < 0) {
|
|
350
|
+
return [marker, ...turn.items];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return [
|
|
354
|
+
...turn.items.slice(0, promptIndex + 1),
|
|
355
|
+
marker,
|
|
356
|
+
...turn.items.slice(promptIndex + 1),
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function updateActiveTurn(
|
|
361
|
+
state: TuiProjectionState,
|
|
362
|
+
event: AgentEvent,
|
|
363
|
+
policy: TuiProjectionPolicy,
|
|
364
|
+
update: (turn: TuiTurnProjection) => TuiTurnProjection,
|
|
365
|
+
): TuiProjectionState {
|
|
366
|
+
const active = requireActiveTurn(state, event);
|
|
367
|
+
return {
|
|
368
|
+
...state,
|
|
369
|
+
status: "running",
|
|
370
|
+
activeTurn: limitTurnItems(update(active), policy.itemLimitPerTurn),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function requireActiveTurn(
|
|
375
|
+
state: TuiProjectionState,
|
|
376
|
+
event: AgentEvent,
|
|
377
|
+
): TuiTurnProjection {
|
|
378
|
+
const active = state.activeTurn;
|
|
379
|
+
if (active === undefined) {
|
|
380
|
+
throw new Error(`Cannot project ${event.type} without an active turn.`);
|
|
381
|
+
}
|
|
382
|
+
if (event.turnId !== active.turnId) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Event ${event.type} belongs to turn ${event.turnId}, expected ${active.turnId}.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return active;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function appendTurnItem(
|
|
391
|
+
turn: TuiTurnProjection,
|
|
392
|
+
item: TimelineItem,
|
|
393
|
+
): TuiTurnProjection {
|
|
394
|
+
return { ...turn, items: [...turn.items, item] };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function updateTurnItem(
|
|
398
|
+
turn: TuiTurnProjection,
|
|
399
|
+
ref: string,
|
|
400
|
+
update: (item: TimelineItem) => TimelineItem,
|
|
401
|
+
): TuiTurnProjection {
|
|
402
|
+
const index = findLastItemIndex(turn.items, ref);
|
|
403
|
+
if (index < 0) {
|
|
404
|
+
throw new Error(`Cannot update missing TUI timeline item ${ref}.`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const items = [...turn.items];
|
|
408
|
+
const item = items[index];
|
|
409
|
+
if (item === undefined) {
|
|
410
|
+
throw new Error(`TUI timeline item ${ref} disappeared during update.`);
|
|
411
|
+
}
|
|
412
|
+
items[index] = update(item);
|
|
413
|
+
return { ...turn, items };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function findLastItemIndex(items: TimelineItem[], ref: string): number {
|
|
417
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
418
|
+
if (items[index]?.ref === ref) {
|
|
419
|
+
return index;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return -1;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function limitTurnItems(turn: TuiTurnProjection, limit: number): TuiTurnProjection {
|
|
426
|
+
if (turn.items.length <= limit) {
|
|
427
|
+
return turn;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const retained = new Set<number>();
|
|
431
|
+
const promptIndex = turn.items.findIndex((item) => item.label === "prompt");
|
|
432
|
+
if (promptIndex >= 0) {
|
|
433
|
+
retained.add(promptIndex);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (let index = turn.items.length - 1; index >= 0; index -= 1) {
|
|
437
|
+
if (retained.size >= limit) {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
if (turn.items[index]?.status === "running") {
|
|
441
|
+
retained.add(index);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
for (let index = turn.items.length - 1; index >= 0; index -= 1) {
|
|
446
|
+
if (retained.size >= limit) {
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
retained.add(index);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const items = turn.items.filter((_item, index) => retained.has(index));
|
|
453
|
+
return {
|
|
454
|
+
...turn,
|
|
455
|
+
items,
|
|
456
|
+
omittedItemCount: turn.omittedItemCount + turn.items.length - items.length,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function finishActiveTurn(
|
|
461
|
+
state: TuiProjectionState,
|
|
462
|
+
event: AgentEvent,
|
|
463
|
+
turn: TuiTurnProjection,
|
|
464
|
+
turnStatus: Exclude<TuiTurnProjection["status"], "running">,
|
|
465
|
+
policy: TuiProjectionPolicy,
|
|
466
|
+
terminalState: Pick<TuiProjectionState, "status" | "finalText" | "error">,
|
|
467
|
+
): TuiProjectionState {
|
|
468
|
+
const workedForMs = turnDurationMs(turn.startedAt, event.timestamp);
|
|
469
|
+
const finished = { ...turn, status: turnStatus, workedForMs };
|
|
470
|
+
const allRecent = [...state.recentTurns, finished];
|
|
471
|
+
const omittedNow = Math.max(0, allRecent.length - policy.recentTurnLimit);
|
|
472
|
+
const recentTurns =
|
|
473
|
+
policy.recentTurnLimit === 0 ? [] : allRecent.slice(-policy.recentTurnLimit);
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
...state,
|
|
477
|
+
...terminalState,
|
|
478
|
+
activeTurn: undefined,
|
|
479
|
+
recentTurns,
|
|
480
|
+
omittedTurnCount: state.omittedTurnCount + omittedNow,
|
|
481
|
+
workedForMs,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function applyTurnCancellation(
|
|
486
|
+
turn: TuiTurnProjection,
|
|
487
|
+
event: Extract<AgentEvent, { type: "turn.cancelled" }>,
|
|
488
|
+
): TuiTurnProjection {
|
|
489
|
+
if (event.data.cancellation.phase === "model_request") {
|
|
490
|
+
return updateTurnItem(
|
|
491
|
+
turn,
|
|
492
|
+
modelRequestRef(event.data.cancellation.iterationId),
|
|
493
|
+
(item) => ({
|
|
494
|
+
...item,
|
|
495
|
+
text: `${item.text} -> cancelled`,
|
|
496
|
+
status: "cancelled",
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (
|
|
502
|
+
event.data.cancellation.phase === "tool_execution" &&
|
|
503
|
+
event.data.cancellation.toolCallId !== undefined
|
|
504
|
+
) {
|
|
505
|
+
return updateTurnItem(
|
|
506
|
+
turn,
|
|
507
|
+
toolCallRef(event.data.cancellation.toolCallId),
|
|
508
|
+
(item) => ({
|
|
509
|
+
...item,
|
|
510
|
+
text: `${item.text} -> cancelled`,
|
|
511
|
+
status: "cancelled",
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return appendTurnItem(turn, {
|
|
517
|
+
id: `turn-${turn.turnId}-cancelled-${event.eventSequence}`,
|
|
518
|
+
text: "turn cancelled",
|
|
519
|
+
status: "cancelled",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function appendNotice(
|
|
524
|
+
state: TuiProjectionState,
|
|
525
|
+
notice: TimelineItem,
|
|
526
|
+
limit: number,
|
|
527
|
+
): TuiProjectionState {
|
|
528
|
+
const notices = [...state.notices, notice];
|
|
529
|
+
return {
|
|
530
|
+
...state,
|
|
531
|
+
notices: limit === 0 ? [] : notices.slice(-limit),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function turnDurationMs(startedAt: string, finishedAt: string): number {
|
|
536
|
+
return Math.max(0, parseEventTimestamp(finishedAt) - parseEventTimestamp(startedAt));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function parseEventTimestamp(value: string): number {
|
|
540
|
+
const timestamp = Date.parse(value);
|
|
541
|
+
if (!Number.isFinite(timestamp)) {
|
|
542
|
+
throw new Error(`Invalid event timestamp: ${value}`);
|
|
543
|
+
}
|
|
544
|
+
return timestamp;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function requireEventString(value: string | undefined, name: string): string {
|
|
548
|
+
if (value === undefined || value.trim() === "") {
|
|
549
|
+
throw new Error(`${name} must be present.`);
|
|
550
|
+
}
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function requireEventNumber(value: number | undefined, name: string): number {
|
|
555
|
+
if (!Number.isInteger(value) || value === undefined || value < 1) {
|
|
556
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
557
|
+
}
|
|
558
|
+
return value;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function modelRequestRef(iterationId: string | undefined): string {
|
|
562
|
+
return `model-request-${requireEventString(iterationId, "model request iterationId")}`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function toolCallRef(callId: string): string {
|
|
566
|
+
return `tool-call-${callId}`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function completedModelRequestText(
|
|
570
|
+
iterationNumber: number,
|
|
571
|
+
toolCallCount: number,
|
|
572
|
+
): string {
|
|
573
|
+
if (toolCallCount > 0) {
|
|
574
|
+
return `model iteration ${iterationNumber} -> ${toolCallCount} tool call${toolCallCount === 1 ? "" : "s"}`;
|
|
575
|
+
}
|
|
576
|
+
return `model iteration ${iterationNumber} -> assistant response`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function toolCallStartedProjection(input: {
|
|
580
|
+
name: string;
|
|
581
|
+
args: unknown;
|
|
582
|
+
}): Pick<TimelineItem, "text" | "bash"> {
|
|
583
|
+
return {
|
|
584
|
+
text: toolCallSummary(input),
|
|
585
|
+
...toolStartedBashDetail(input),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function toolRawResultProjection(
|
|
590
|
+
input: { name: string; args: unknown },
|
|
591
|
+
raw: ToolRawResult,
|
|
592
|
+
): Pick<TimelineItem, "text" | "bash" | "diff" | "diffTruncated"> {
|
|
593
|
+
return {
|
|
594
|
+
text: toolRawResultSummary(input.name, input.args, raw),
|
|
595
|
+
...toolRawResultDiff(raw),
|
|
596
|
+
...toolRawResultBashDetail(raw),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function toolCallSummary(input: { name: string; args: unknown }): string {
|
|
601
|
+
if (input.name === "Bash") {
|
|
602
|
+
return `Bash ${bashDescription(input.args) ?? ""}`.trim();
|
|
603
|
+
}
|
|
604
|
+
if (input.name === "Glob") {
|
|
605
|
+
return `Glob ${toolPattern(input.args) ?? ""}`.trim();
|
|
606
|
+
}
|
|
607
|
+
if (input.name === "Grep") {
|
|
608
|
+
return `Grep ${toolPattern(input.args) ?? ""}`.trim();
|
|
609
|
+
}
|
|
610
|
+
if (input.name === "WebSearch") {
|
|
611
|
+
return `WebSearch ${toolQuery(input.args) ?? ""}`.trim();
|
|
612
|
+
}
|
|
613
|
+
if (input.name === "WebFetch") {
|
|
614
|
+
return `WebFetch ${toolUrl(input.args) ?? ""}`.trim();
|
|
615
|
+
}
|
|
616
|
+
if (input.name === "TaskOutput" || input.name === "TaskStop") {
|
|
617
|
+
return `${input.name} ${toolTaskId(input.args) ?? ""}`.trim();
|
|
618
|
+
}
|
|
619
|
+
if (input.name === "TaskList") {
|
|
620
|
+
return "TaskList";
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const filePath = toolPath(input.args);
|
|
624
|
+
return `${input.name}${filePath === undefined ? "" : ` ${filePath}`}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult): string {
|
|
628
|
+
const base = toolCallSummary({ name, args });
|
|
629
|
+
if (!raw.ok && raw.error !== undefined) {
|
|
630
|
+
return `${base} -> ${boundedToolError(raw.error)}`;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
switch (raw.kind) {
|
|
634
|
+
case "glob":
|
|
635
|
+
return raw.ok && raw.matchCount !== undefined
|
|
636
|
+
? `${base} -> ${raw.matchCount} match${raw.matchCount === 1 ? "" : "es"}`
|
|
637
|
+
: base;
|
|
638
|
+
case "grep":
|
|
639
|
+
if (raw.mode === "content" && raw.numLines !== undefined) {
|
|
640
|
+
return `${base} -> ${raw.numLines} line${raw.numLines === 1 ? "" : "s"}`;
|
|
641
|
+
}
|
|
642
|
+
if (
|
|
643
|
+
raw.mode === "count" &&
|
|
644
|
+
raw.numMatches !== undefined &&
|
|
645
|
+
raw.numFiles !== undefined
|
|
646
|
+
) {
|
|
647
|
+
return `${base} -> ${raw.numMatches} match${raw.numMatches === 1 ? "" : "es"} across ${raw.numFiles} file${raw.numFiles === 1 ? "" : "s"}`;
|
|
648
|
+
}
|
|
649
|
+
return `${base} -> ${raw.numFiles} file${raw.numFiles === 1 ? "" : "s"}`;
|
|
650
|
+
case "read":
|
|
651
|
+
if (
|
|
652
|
+
raw.startLine !== undefined &&
|
|
653
|
+
raw.endLine !== undefined &&
|
|
654
|
+
raw.totalLines !== undefined
|
|
655
|
+
) {
|
|
656
|
+
return `${base} -> lines ${raw.startLine}-${raw.endLine} of ${raw.totalLines}`;
|
|
657
|
+
}
|
|
658
|
+
return base;
|
|
659
|
+
case "write":
|
|
660
|
+
case "edit": {
|
|
661
|
+
const patch = raw.patch;
|
|
662
|
+
if (patch !== undefined) {
|
|
663
|
+
const changes = countPatchChanges(patch);
|
|
664
|
+
const created = raw.created === true ? " (new file)" : "";
|
|
665
|
+
return `${base} -> +${changes.additions} -${changes.deletions}${created}`;
|
|
666
|
+
}
|
|
667
|
+
return raw.bytesWritten === undefined
|
|
668
|
+
? base
|
|
669
|
+
: `${base} -> ${raw.bytesWritten} bytes`;
|
|
670
|
+
}
|
|
671
|
+
case "web_search":
|
|
672
|
+
return raw.resultCount === undefined
|
|
673
|
+
? base
|
|
674
|
+
: `${base} -> ${raw.resultCount} result${raw.resultCount === 1 ? "" : "s"}`;
|
|
675
|
+
case "web_fetch":
|
|
676
|
+
if (raw.redirectUrl !== undefined) {
|
|
677
|
+
return `${base} -> redirected`;
|
|
678
|
+
}
|
|
679
|
+
return raw.route === undefined
|
|
680
|
+
? base
|
|
681
|
+
: `${base} -> ok (${raw.route}${raw.refined === true ? ", refined" : ""})`;
|
|
682
|
+
case "recall":
|
|
683
|
+
if (!raw.ok) {
|
|
684
|
+
return base;
|
|
685
|
+
}
|
|
686
|
+
return raw.mode === "search"
|
|
687
|
+
? `${base} -> ${raw.page.hits.length} historical match${raw.page.hits.length === 1 ? "" : "es"}`
|
|
688
|
+
: `${base} -> ${raw.page.returnedBytes} historical bytes`;
|
|
689
|
+
case "task_list":
|
|
690
|
+
return `${base} -> ${raw.tasks.length} task${raw.tasks.length === 1 ? "" : "s"}, ${raw.runningCount} running`;
|
|
691
|
+
case "task_output":
|
|
692
|
+
if (raw.status !== undefined && raw.outputLines !== undefined) {
|
|
693
|
+
return `${base} -> ${raw.status}, ${raw.outputLines} line${raw.outputLines === 1 ? "" : "s"}`;
|
|
694
|
+
}
|
|
695
|
+
return base;
|
|
696
|
+
case "task_stop": {
|
|
697
|
+
if (raw.status === undefined) {
|
|
698
|
+
return base;
|
|
699
|
+
}
|
|
700
|
+
const signal = raw.task?.signal;
|
|
701
|
+
return `${base} -> ${raw.status}${signal === undefined ? "" : ` (${signal})`}`;
|
|
702
|
+
}
|
|
703
|
+
case "bash":
|
|
704
|
+
if (raw.status === "running") {
|
|
705
|
+
return raw.outputFilePath === undefined
|
|
706
|
+
? `${base} -> running`
|
|
707
|
+
: `${base} -> running ${raw.outputFilePath}`;
|
|
708
|
+
}
|
|
709
|
+
return raw.exitCode === undefined ? base : `${base} -> exit ${raw.exitCode}`;
|
|
710
|
+
case "mcp":
|
|
711
|
+
case "generic":
|
|
712
|
+
return base;
|
|
713
|
+
default:
|
|
714
|
+
return assertNever(raw);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function boundedToolError(error: string): string {
|
|
719
|
+
const limit = 1_000;
|
|
720
|
+
return error.length <= limit ? error : `${error.slice(0, limit)}…`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function toolStartedBashDetail(call: {
|
|
724
|
+
name: string;
|
|
725
|
+
args: unknown;
|
|
726
|
+
}): Pick<TimelineItem, "bash"> {
|
|
727
|
+
if (call.name !== "Bash") {
|
|
728
|
+
return {};
|
|
729
|
+
}
|
|
730
|
+
const command = bashCommandFromArgs(call.args);
|
|
731
|
+
return command === undefined ? {} : { bash: { command } };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash"> {
|
|
735
|
+
switch (raw.kind) {
|
|
736
|
+
case "bash":
|
|
737
|
+
case "task_output": {
|
|
738
|
+
const detail = bashResultDetail(raw);
|
|
739
|
+
return detail === undefined ? {} : { bash: detail };
|
|
740
|
+
}
|
|
741
|
+
case "read":
|
|
742
|
+
case "write":
|
|
743
|
+
case "edit":
|
|
744
|
+
case "glob":
|
|
745
|
+
case "grep":
|
|
746
|
+
case "task_list":
|
|
747
|
+
case "task_stop":
|
|
748
|
+
case "web_search":
|
|
749
|
+
case "web_fetch":
|
|
750
|
+
case "recall":
|
|
751
|
+
case "mcp":
|
|
752
|
+
case "generic":
|
|
753
|
+
return {};
|
|
754
|
+
default:
|
|
755
|
+
return assertNever(raw);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function toolRawResultDiff(
|
|
760
|
+
raw: ToolRawResult,
|
|
761
|
+
): Pick<TimelineItem, "diff" | "diffTruncated"> {
|
|
762
|
+
switch (raw.kind) {
|
|
763
|
+
case "write":
|
|
764
|
+
case "edit":
|
|
765
|
+
return raw.patch === undefined || raw.patch.length === 0
|
|
766
|
+
? {}
|
|
767
|
+
: {
|
|
768
|
+
diff: raw.patch.map((hunk) => ({
|
|
769
|
+
...hunk,
|
|
770
|
+
lines: [...hunk.lines],
|
|
771
|
+
})),
|
|
772
|
+
diffTruncated: raw.patchTruncated === true,
|
|
773
|
+
};
|
|
774
|
+
case "read":
|
|
775
|
+
case "glob":
|
|
776
|
+
case "grep":
|
|
777
|
+
case "bash":
|
|
778
|
+
case "task_list":
|
|
779
|
+
case "task_output":
|
|
780
|
+
case "task_stop":
|
|
781
|
+
case "web_search":
|
|
782
|
+
case "web_fetch":
|
|
783
|
+
case "recall":
|
|
784
|
+
case "mcp":
|
|
785
|
+
case "generic":
|
|
786
|
+
return {};
|
|
787
|
+
default:
|
|
788
|
+
return assertNever(raw);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function upsertBackgroundTask(
|
|
793
|
+
tasks: ShellTaskSnapshot[],
|
|
794
|
+
task: ShellTaskSnapshot,
|
|
795
|
+
completedTaskLimit: number,
|
|
796
|
+
): ShellTaskSnapshot[] {
|
|
797
|
+
const snapshot = cloneTaskSnapshot(task);
|
|
798
|
+
const updated = [
|
|
799
|
+
...tasks.filter((candidate) => candidate.taskId !== snapshot.taskId),
|
|
800
|
+
snapshot,
|
|
801
|
+
];
|
|
802
|
+
const active = updated.filter((candidate) => isActiveTask(candidate.status));
|
|
803
|
+
const terminal = updated
|
|
804
|
+
.filter((candidate) => !isActiveTask(candidate.status))
|
|
805
|
+
.sort((left, right) =>
|
|
806
|
+
(right.endedAt ?? right.startedAt).localeCompare(left.endedAt ?? left.startedAt),
|
|
807
|
+
)
|
|
808
|
+
.slice(0, completedTaskLimit);
|
|
809
|
+
return [...active, ...terminal].sort((left, right) =>
|
|
810
|
+
right.startedAt.localeCompare(left.startedAt),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function cloneTaskSnapshot(task: ShellTaskSnapshot): ShellTaskSnapshot {
|
|
815
|
+
return { ...task, origin: { ...task.origin } };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function isActiveTask(status: ShellTaskStatus): boolean {
|
|
819
|
+
return status === "running" || status === "stopping";
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function bashDescription(args: unknown): string | undefined {
|
|
823
|
+
const argsRecord = asRecord(args);
|
|
824
|
+
return (
|
|
825
|
+
stringProperty(argsRecord, "description") ?? stringProperty(argsRecord, "command")
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function toolPattern(args: unknown): string | undefined {
|
|
830
|
+
return stringProperty(asRecord(args), "pattern");
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function toolQuery(args: unknown): string | undefined {
|
|
834
|
+
return stringProperty(asRecord(args), "query");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function toolUrl(args: unknown): string | undefined {
|
|
838
|
+
return stringProperty(asRecord(args), "url");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function toolPath(args: unknown): string | undefined {
|
|
842
|
+
const record = asRecord(args);
|
|
843
|
+
return stringProperty(record, "file_path");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function toolTaskId(args: unknown): string | undefined {
|
|
847
|
+
return stringProperty(asRecord(args), "task_id");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function stringProperty(
|
|
851
|
+
record: Record<string, unknown>,
|
|
852
|
+
property: string,
|
|
853
|
+
): string | undefined {
|
|
854
|
+
return typeof record[property] === "string" ? record[property] : undefined;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
858
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
859
|
+
? (value as Record<string, unknown>)
|
|
860
|
+
: {};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function assertNever(value: never): never {
|
|
864
|
+
throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
|
|
865
|
+
}
|