openhorse 0.2.12 → 0.2.13

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 (79) hide show
  1. package/dist/commands/index.d.ts.map +1 -1
  2. package/dist/commands/index.js +597 -12
  3. package/dist/commands/index.js.map +1 -1
  4. package/dist/commands/types.d.ts +3 -1
  5. package/dist/commands/types.d.ts.map +1 -1
  6. package/dist/commands/types.js.map +1 -1
  7. package/dist/core/checkpoint.d.ts +1 -0
  8. package/dist/core/checkpoint.d.ts.map +1 -1
  9. package/dist/core/checkpoint.js +59 -10
  10. package/dist/core/checkpoint.js.map +1 -1
  11. package/dist/core/tool-artifacts.d.ts +10 -0
  12. package/dist/core/tool-artifacts.d.ts.map +1 -1
  13. package/dist/core/tool-artifacts.js +81 -3
  14. package/dist/core/tool-artifacts.js.map +1 -1
  15. package/dist/framework/index.d.ts +2 -2
  16. package/dist/framework/index.d.ts.map +1 -1
  17. package/dist/framework/index.js +5 -1
  18. package/dist/framework/index.js.map +1 -1
  19. package/dist/framework/query.d.ts +87 -2
  20. package/dist/framework/query.d.ts.map +1 -1
  21. package/dist/framework/query.js +281 -14
  22. package/dist/framework/query.js.map +1 -1
  23. package/dist/framework/tool-scheduler.d.ts +9 -0
  24. package/dist/framework/tool-scheduler.d.ts.map +1 -1
  25. package/dist/framework/tool-scheduler.js +34 -0
  26. package/dist/framework/tool-scheduler.js.map +1 -1
  27. package/dist/ink-ui/components/StatusLine.d.ts.map +1 -1
  28. package/dist/ink-ui/components/StatusLine.js +7 -1
  29. package/dist/ink-ui/components/StatusLine.js.map +1 -1
  30. package/dist/runtime/agent-runtime-protocol.d.ts +13 -1
  31. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  32. package/dist/runtime/agent-runtime-protocol.js +24 -0
  33. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  34. package/dist/runtime/chat-controller.d.ts +3 -0
  35. package/dist/runtime/chat-controller.d.ts.map +1 -1
  36. package/dist/runtime/chat-controller.js +907 -2
  37. package/dist/runtime/chat-controller.js.map +1 -1
  38. package/dist/runtime/loop-budget.d.ts +5 -0
  39. package/dist/runtime/loop-budget.d.ts.map +1 -0
  40. package/dist/runtime/loop-budget.js +77 -0
  41. package/dist/runtime/loop-budget.js.map +1 -0
  42. package/dist/runtime/ui-events.d.ts +34 -1
  43. package/dist/runtime/ui-events.d.ts.map +1 -1
  44. package/dist/runtime/ui-events.js.map +1 -1
  45. package/dist/services/config-dir.d.ts +2 -0
  46. package/dist/services/config-dir.d.ts.map +1 -1
  47. package/dist/services/config-dir.js +5 -0
  48. package/dist/services/config-dir.js.map +1 -1
  49. package/dist/services/config.d.ts +4 -2
  50. package/dist/services/config.d.ts.map +1 -1
  51. package/dist/services/config.js +19 -0
  52. package/dist/services/config.js.map +1 -1
  53. package/dist/services/global-config.d.ts +17 -0
  54. package/dist/services/global-config.d.ts.map +1 -1
  55. package/dist/services/global-config.js.map +1 -1
  56. package/dist/services/llm.d.ts +16 -0
  57. package/dist/services/llm.d.ts.map +1 -1
  58. package/dist/services/llm.js +56 -11
  59. package/dist/services/llm.js.map +1 -1
  60. package/dist/services/redaction.d.ts +2 -0
  61. package/dist/services/redaction.d.ts.map +1 -0
  62. package/dist/services/redaction.js +27 -0
  63. package/dist/services/redaction.js.map +1 -0
  64. package/dist/services/session-index.d.ts.map +1 -1
  65. package/dist/services/session-index.js +7 -3
  66. package/dist/services/session-index.js.map +1 -1
  67. package/dist/services/session-storage.d.ts +98 -0
  68. package/dist/services/session-storage.d.ts.map +1 -1
  69. package/dist/services/session-storage.js +153 -2
  70. package/dist/services/session-storage.js.map +1 -1
  71. package/dist/services/verification-profile.d.ts +36 -0
  72. package/dist/services/verification-profile.d.ts.map +1 -0
  73. package/dist/services/verification-profile.js +239 -0
  74. package/dist/services/verification-profile.js.map +1 -0
  75. package/dist/services/workspace-state.d.ts +25 -0
  76. package/dist/services/workspace-state.d.ts.map +1 -0
  77. package/dist/services/workspace-state.js +115 -0
  78. package/dist/services/workspace-state.js.map +1 -0
  79. package/package.json +1 -1
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.InkChatController = exports.AgentChatController = void 0;
4
37
  exports.sessionMessagesToTranscriptEntries = sessionMessagesToTranscriptEntries;
@@ -8,17 +41,26 @@ exports.loadSessionIntoRuntime = loadSessionIntoRuntime;
8
41
  exports.closeSession = closeSession;
9
42
  const commands_1 = require("../commands");
10
43
  const parser_1 = require("../commands/parser");
44
+ const path = __importStar(require("path"));
11
45
  const session_storage_1 = require("../services/session-storage");
12
46
  const config_1 = require("../services/config");
13
47
  const framework_1 = require("../framework");
14
48
  const harness_1 = require("../harness");
15
49
  const tools_1 = require("../tools");
50
+ const tool_serializer_1 = require("../framework/tool-serializer");
51
+ const tool_artifacts_1 = require("../core/tool-artifacts");
52
+ const checkpoint_1 = require("../core/checkpoint");
16
53
  const skills_1 = require("../skills");
17
54
  const file_context_1 = require("../services/file-context");
18
55
  const prompt_context_1 = require("../services/prompt-context");
56
+ const workspace_state_1 = require("../services/workspace-state");
57
+ const verification_profile_1 = require("../services/verification-profile");
19
58
  const ui_events_1 = require("./ui-events");
20
59
  const agent_status_1 = require("./agent-status");
60
+ const loop_budget_1 = require("./loop-budget");
21
61
  const ANSI_PATTERN = /\x1b\[[0-9;?]*[A-Za-z]/g;
62
+ const LOCAL_FAST_PATH_INLINE_OUTPUT_BYTES = 2048;
63
+ const TRACE_ARGS_ARTIFACT_THRESHOLD_BYTES = 160;
22
64
  function stripAnsi(text) {
23
65
  return text.replace(ANSI_PATTERN, '');
24
66
  }
@@ -64,6 +106,316 @@ function compactToolArgs(args, maxLength = 160) {
64
106
  }
65
107
  return '';
66
108
  }
109
+ function fullToolArgsForTrace(name, args) {
110
+ if (name === 'exec_command' && typeof args.command === 'string') {
111
+ return `$ ${args.command}`;
112
+ }
113
+ try {
114
+ return JSON.stringify(args, null, 2);
115
+ }
116
+ catch {
117
+ return compactToolArgs(args, 2048);
118
+ }
119
+ }
120
+ function buildTraceArgsDetails(projectPath, name, args) {
121
+ const argsSummary = compactToolArgs(args);
122
+ const fullArgs = (0, session_storage_1.redactTraceText)(fullToolArgsForTrace(name, args)).trim();
123
+ const argsBytes = byteLength(fullArgs);
124
+ if (!projectPath
125
+ || !fullArgs
126
+ || fullArgs === (0, session_storage_1.redactTraceText)(argsSummary)
127
+ || argsBytes <= TRACE_ARGS_ARTIFACT_THRESHOLD_BYTES) {
128
+ return { argsSummary };
129
+ }
130
+ const artifact = (0, tool_artifacts_1.storeArtifact)(projectPath, `${name}-args`, fullArgs, argsBytes);
131
+ return artifact
132
+ ? { argsSummary, argsArtifactId: artifact.id, argsBytes }
133
+ : { argsSummary };
134
+ }
135
+ function parseToolCallArgsForRuntime(toolCall) {
136
+ try {
137
+ const parsed = JSON.parse(toolCall.function.arguments || '{}');
138
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
139
+ ? parsed
140
+ : null;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ function resolveProjectScopedPath(cwd, filePath) {
147
+ const absolute = path.resolve(cwd, filePath);
148
+ const relative = path.relative(cwd, absolute);
149
+ if (relative.startsWith('..') || path.isAbsolute(relative))
150
+ return null;
151
+ return absolute;
152
+ }
153
+ function checkpointTargetsFromToolCalls(cwd, toolCalls) {
154
+ const targets = new Set();
155
+ for (const toolCall of toolCalls) {
156
+ const name = toolCall.function.name;
157
+ if (name !== 'write_file' && name !== 'edit_file')
158
+ continue;
159
+ const args = parseToolCallArgsForRuntime(toolCall);
160
+ if (!args || typeof args.path !== 'string')
161
+ continue;
162
+ if (name === 'edit_file' && args.preview === true)
163
+ continue;
164
+ const target = resolveProjectScopedPath(cwd, args.path);
165
+ if (target)
166
+ targets.add(target);
167
+ }
168
+ return Array.from(targets);
169
+ }
170
+ function createPreToolCheckpoint(events, sessionId, turnId, checkpointId, cwd, toolCalls) {
171
+ const targets = checkpointTargetsFromToolCalls(cwd, toolCalls);
172
+ if (targets.length === 0)
173
+ return false;
174
+ const checkpoint = (0, checkpoint_1.createCheckpoint)(cwd, checkpointId, targets);
175
+ if (!sessionId)
176
+ return true;
177
+ const relativeTargets = targets.map(target => path.relative(cwd, target));
178
+ recordTraceEvent(events, sessionId, {
179
+ turnId,
180
+ type: 'checkpoint',
181
+ checkpointId,
182
+ checkpointFileCount: checkpoint?.files.length ?? 0,
183
+ checkpointFiles: checkpoint?.files.map(file => file.path) ?? [],
184
+ workspaceFiles: relativeTargets,
185
+ note: checkpoint
186
+ ? 'pre_edit_checkpoint'
187
+ : 'pre_edit_checkpoint_skipped',
188
+ });
189
+ return true;
190
+ }
191
+ function byteLength(text) {
192
+ return Buffer.byteLength(text, 'utf8');
193
+ }
194
+ function traceTurnId(turnId) {
195
+ return turnId == null ? `turn-${Date.now()}` : String(turnId);
196
+ }
197
+ function compactTraceError(error) {
198
+ const message = error instanceof Error ? error.message : String(error);
199
+ return compactMiddle(message, 240);
200
+ }
201
+ function getLastRequestDiagnostics(llm) {
202
+ if (!llm)
203
+ return undefined;
204
+ const reader = llm.getLastRequestDiagnostics;
205
+ return typeof reader === 'function' ? reader.call(llm) : undefined;
206
+ }
207
+ function compactPathList(paths, maxItems = 40) {
208
+ return paths.slice(0, maxItems);
209
+ }
210
+ function formatWorkspaceFileForTrace(file) {
211
+ const metadata = [
212
+ typeof file.sizeBytes === 'number' ? `${file.sizeBytes}B` : '',
213
+ typeof file.mtimeMs === 'number' ? `mtime=${file.mtimeMs}` : '',
214
+ ].filter(Boolean).join(' ');
215
+ return `${file.status} ${file.path}${metadata ? ` (${metadata})` : ''}`;
216
+ }
217
+ function appendWorkspaceSnapshotTrace(events, sessionId, turnId, phase, snapshot) {
218
+ if (!sessionId)
219
+ return;
220
+ recordTraceEvent(events, sessionId, {
221
+ turnId,
222
+ type: 'workspace_snapshot',
223
+ workspacePhase: phase,
224
+ workspaceGitAvailable: snapshot.gitAvailable,
225
+ workspaceDirty: snapshot.dirty,
226
+ workspaceBranch: snapshot.branch,
227
+ workspaceFileCount: snapshot.fileCount,
228
+ workspaceFiles: compactPathList(snapshot.files.map(formatWorkspaceFileForTrace)),
229
+ error: snapshot.error ? compactMiddle(snapshot.error, 240) : undefined,
230
+ });
231
+ }
232
+ function appendWorkspaceDeltaTrace(events, sessionId, turnId, before, after) {
233
+ const delta = (0, workspace_state_1.diffWorkspaceSnapshots)(before, after);
234
+ if (sessionId) {
235
+ recordTraceEvent(events, sessionId, {
236
+ turnId,
237
+ type: 'workspace_delta',
238
+ workspaceFileCount: delta.filesAfterTurn.length,
239
+ workspaceFiles: compactPathList(delta.filesAfterTurn),
240
+ workspaceNewByTurn: compactPathList(delta.newFilesByTurn),
241
+ workspaceChangedByTurn: compactPathList(delta.changedByTurn),
242
+ workspaceModifiedPreExistingByTurn: compactPathList(delta.modifiedPreExistingByTurn),
243
+ workspaceResolvedByTurn: compactPathList(delta.resolvedByTurn),
244
+ note: `pre_existing=${delta.preExistingFiles.length}`,
245
+ });
246
+ }
247
+ return delta;
248
+ }
249
+ function workspaceDeltaHasTurnChanges(delta) {
250
+ return delta.newFilesByTurn.length > 0
251
+ || delta.changedByTurn.length > 0
252
+ || delta.resolvedByTurn.length > 0;
253
+ }
254
+ function formatFailureRecoveryNotice(turnId, delta, checkpointIds) {
255
+ const files = compactPathList([
256
+ ...delta.newFilesByTurn,
257
+ ...delta.changedByTurn,
258
+ ...delta.resolvedByTurn,
259
+ ], 8);
260
+ const fileText = files.length > 0
261
+ ? files.join(', ')
262
+ : 'workspace changes recorded';
263
+ const checkpointText = checkpointIds.length > 0
264
+ ? ` Checkpoints: ${checkpointIds.join(', ')}. Preview rollback with /checkpoint restore <id>; restore each listed checkpoint if multiple.`
265
+ : '';
266
+ return `Turn failed after modifying files: ${fileText}. Inspect /trace ${turnId}.${checkpointText}`;
267
+ }
268
+ function appendVerificationProfileTrace(events, sessionId, turnId, profile) {
269
+ if (!sessionId || profile.changedFiles.length === 0)
270
+ return;
271
+ recordTraceEvent(events, sessionId, {
272
+ turnId,
273
+ type: 'verification_profile',
274
+ verificationProfile: profile.profile,
275
+ verificationRequired: profile.required,
276
+ verificationCommands: compactPathList(profile.commands, 8),
277
+ verificationChangedFiles: compactPathList(profile.changedFiles),
278
+ note: profile.reason,
279
+ });
280
+ }
281
+ function appendVerificationResultTrace(events, sessionId, turnId, result) {
282
+ if (!sessionId)
283
+ return;
284
+ recordTraceEvent(events, sessionId, {
285
+ turnId,
286
+ type: 'verification_result',
287
+ verificationCommand: result.command,
288
+ verificationPassed: result.success,
289
+ outputBytes: result.outputBytes,
290
+ error: result.error ? compactMiddle(result.error, 240) : undefined,
291
+ });
292
+ }
293
+ function appendVerificationSummaryTrace(events, sessionId, turnId, summary, changedFiles) {
294
+ if (!sessionId || changedFiles.length === 0)
295
+ return;
296
+ recordTraceEvent(events, sessionId, {
297
+ turnId,
298
+ type: 'verification_summary',
299
+ verificationProfile: summary.profile,
300
+ verificationRequired: summary.required,
301
+ verificationCommands: compactPathList(summary.commandsRun, 12),
302
+ verificationPassedCommands: compactPathList(summary.passedCommands, 12),
303
+ verificationFailedCommands: compactPathList(summary.failedCommands, 12),
304
+ verificationMissingCommands: compactPathList(summary.missingCommands, 12),
305
+ verificationChangedFiles: compactPathList(changedFiles),
306
+ verificationClaimAllowed: summary.claimAllowed,
307
+ note: summary.skippedReason,
308
+ });
309
+ }
310
+ function compactVerificationCommands(commands, maxItems = 12) {
311
+ return commands.slice(0, maxItems).map(session_storage_1.redactTraceText);
312
+ }
313
+ function withVerificationLoopStats(stats, summary) {
314
+ return {
315
+ ...stats,
316
+ verificationProfile: summary.profile,
317
+ verificationRequired: summary.required,
318
+ verificationClaimAllowed: summary.claimAllowed,
319
+ verificationPassedCommands: compactVerificationCommands(summary.passedCommands),
320
+ verificationFailedCommands: compactVerificationCommands(summary.failedCommands),
321
+ verificationMissingCommands: compactVerificationCommands(summary.missingCommands),
322
+ verificationSkippedReason: summary.skippedReason ? (0, session_storage_1.redactTraceText)(summary.skippedReason) : undefined,
323
+ };
324
+ }
325
+ function shouldRecordVerificationLoopStats(profile, summary) {
326
+ return profile.changedFiles.length > 0
327
+ || summary.commandsRun.length > 0
328
+ || summary.passedCommands.length > 0
329
+ || summary.failedCommands.length > 0
330
+ || summary.missingCommands.length > 0;
331
+ }
332
+ function appendPostWorkspaceTrace(events, sessionId, turnId, cwd, before, verificationResults = []) {
333
+ const postWorkspace = (0, workspace_state_1.captureWorkspaceSnapshot)(cwd);
334
+ appendWorkspaceSnapshotTrace(events, sessionId, turnId, 'post_turn', postWorkspace);
335
+ const delta = appendWorkspaceDeltaTrace(events, sessionId, turnId, before, postWorkspace);
336
+ const profile = (0, verification_profile_1.selectVerificationProfile)(cwd, delta.changedByTurn);
337
+ const summary = (0, verification_profile_1.summarizeVerificationState)(profile, verificationResults);
338
+ appendVerificationProfileTrace(events, sessionId, turnId, profile);
339
+ appendVerificationSummaryTrace(events, sessionId, turnId, summary, profile.changedFiles);
340
+ return { delta, profile, summary };
341
+ }
342
+ function appendAssistantNotice(messages, notice) {
343
+ for (let index = messages.length - 1; index >= 0; index--) {
344
+ const message = messages[index];
345
+ if (message.role === 'assistant' && !message.tool_calls) {
346
+ message.content = message.content ? `${message.content}\n\n${notice}` : notice;
347
+ return;
348
+ }
349
+ }
350
+ messages.push({
351
+ role: 'assistant',
352
+ content: notice,
353
+ timestamp: Date.now(),
354
+ });
355
+ }
356
+ function recordTraceEvent(events, sessionId, event) {
357
+ if (!sessionId)
358
+ return null;
359
+ const traceEvent = (0, session_storage_1.appendSessionTraceEvent)(sessionId, event);
360
+ if (traceEvent) {
361
+ events.traceEventRecorded?.(traceEvent);
362
+ }
363
+ return traceEvent;
364
+ }
365
+ function recordProviderTraceEvents(events, sessionId, turnId, stats) {
366
+ if ((stats.providerRetryCount ?? 0) > 0) {
367
+ recordTraceEvent(events, sessionId, {
368
+ turnId,
369
+ type: 'provider_retry',
370
+ providerRetryCount: stats.providerRetryCount,
371
+ providerRetryDelayMs: stats.providerRetryDelayMs,
372
+ providerRetryErrorTypes: stats.providerRetryErrorTypes,
373
+ providerLastRetryErrorType: stats.providerLastRetryErrorType,
374
+ providerLastRetryStatus: stats.providerLastRetryStatus,
375
+ providerFinalModel: stats.providerFinalModel,
376
+ providerUsingFallback: stats.providerUsingFallback,
377
+ });
378
+ }
379
+ if ((stats.providerFallbackCount ?? 0) > 0 || stats.providerUsingFallback) {
380
+ recordTraceEvent(events, sessionId, {
381
+ turnId,
382
+ type: 'provider_fallback',
383
+ providerFallbackCount: stats.providerFallbackCount,
384
+ providerFallbackFromModel: stats.providerFallbackFromModel,
385
+ providerFallbackToModel: stats.providerFallbackToModel,
386
+ providerFinalModel: stats.providerFinalModel,
387
+ providerUsingFallback: stats.providerUsingFallback,
388
+ });
389
+ }
390
+ }
391
+ function toHarnessDiagnostics(state) {
392
+ const stats = state.promptAssemblyStats;
393
+ const redactOptional = (value) => typeof value === 'string' ? (0, session_storage_1.redactTraceText)(value) : undefined;
394
+ const redactList = (values) => values?.slice(0, 6).map(session_storage_1.redactTraceText);
395
+ return {
396
+ taskEpoch: state.taskEpoch,
397
+ rootObjective: redactOptional(state.rootObjective ?? state.contract?.objective),
398
+ activeInstruction: redactOptional(state.activeInstruction ?? state.contract?.userIntent),
399
+ openQuestions: redactList(state.openQuestions),
400
+ diagnostics: redactList(state.diagnostics?.slice(-6)),
401
+ ledgerSize: state.ledger?.length ?? 0,
402
+ evidenceSize: state.evidenceIndex?.length ?? 0,
403
+ turnSummaryCount: state.turnSummaries?.length ?? 0,
404
+ promptAssembly: stats
405
+ ? {
406
+ modelId: stats.modelId,
407
+ estimatedTokens: stats.estimatedTokens,
408
+ budgetTokens: stats.budgetTokens,
409
+ sections: stats.sections.slice(0, 12),
410
+ includedEvidence: stats.includedEvidence.length,
411
+ omittedEvidence: stats.omittedEvidence.length,
412
+ }
413
+ : undefined,
414
+ };
415
+ }
416
+ function emitHarnessDiagnostics(events, state) {
417
+ events.harnessDiagnosticsUpdated?.(toHarnessDiagnostics(state));
418
+ }
67
419
  function toolStartContent(name, args) {
68
420
  if (name === 'exec_command' && typeof args.command === 'string') {
69
421
  return `Running ${name}\n $ ${args.command}`;
@@ -254,8 +606,76 @@ function createAssistantStreamPresenter(events, abortSignal) {
254
606
  activeSegmentText = content;
255
607
  ensureLiveEntry();
256
608
  },
609
+ replaceMessage(content) {
610
+ if (abortSignal?.aborted || !content)
611
+ return;
612
+ activeSegmentText = content;
613
+ ensureLiveEntry();
614
+ },
257
615
  };
258
616
  }
617
+ class LocalFastPathBlockedError extends Error {
618
+ constructor(message) {
619
+ super(message);
620
+ this.name = 'LocalFastPathBlockedError';
621
+ }
622
+ }
623
+ function parseLocalFastPath(input) {
624
+ const text = input.trim();
625
+ if (/^git\s+status$/i.test(text)) {
626
+ return { tool: 'git_status', args: {}, label: 'git status' };
627
+ }
628
+ const readMatch = /^(?:read|读取)\s+(.+)$/i.exec(text);
629
+ const readTarget = readMatch?.[1]?.trim();
630
+ const looksLikePath = Boolean(readTarget)
631
+ && !/\s/.test(readTarget)
632
+ && (/[/\\.]/.test(readTarget) || readTarget.startsWith('~'));
633
+ if (readTarget && looksLikePath) {
634
+ return { tool: 'read_file', args: { path: readTarget }, label: `read ${readTarget}` };
635
+ }
636
+ const grepMatch = /^(?:grep|搜索)\s+(.+)$/i.exec(text);
637
+ if (grepMatch?.[1]?.trim()) {
638
+ return { tool: 'grep', args: { pattern: grepMatch[1].trim() }, label: `grep ${grepMatch[1].trim()}` };
639
+ }
640
+ const runTestMatch = /^(?:run\s+test|运行测试)\s*[::]\s*(.+)$/i.exec(text);
641
+ if (runTestMatch?.[1]?.trim()) {
642
+ return { tool: 'exec_command', args: { command: runTestMatch[1].trim() }, label: `run test: ${runTestMatch[1].trim()}` };
643
+ }
644
+ return null;
645
+ }
646
+ function formatLocalFastPathAssistantContent(action, rawResult, projectPath) {
647
+ const envelope = (0, tool_serializer_1.parseToolResultEnvelope)(rawResult);
648
+ const output = typeof envelope.output === 'string' ? envelope.output.trim() : '';
649
+ const summary = envelope.summary || `${action.tool} ${envelope.success ? 'completed' : 'failed'}`;
650
+ const lines = [
651
+ envelope.success
652
+ ? `Local fast path completed ${action.label}.`
653
+ : `Local fast path failed ${action.label}.`,
654
+ '',
655
+ summary,
656
+ ];
657
+ if (!envelope.success && envelope.error) {
658
+ lines.push(`Error: ${envelope.error}`);
659
+ }
660
+ if (!output) {
661
+ return lines.join('\n');
662
+ }
663
+ let artifactRef = envelope.artifactRef;
664
+ let preview = output;
665
+ const outputBytes = envelope.outputBytes ?? byteLength(output);
666
+ if (byteLength(output) > LOCAL_FAST_PATH_INLINE_OUTPUT_BYTES) {
667
+ if (!artifactRef) {
668
+ const artifact = (0, tool_artifacts_1.storeArtifact)(projectPath, action.tool, output, outputBytes);
669
+ artifactRef = artifact ? { id: artifact.id, outputBytes: artifact.outputBytes } : undefined;
670
+ }
671
+ preview = (0, tool_artifacts_1.truncateForContext)(output, LOCAL_FAST_PATH_INLINE_OUTPUT_BYTES);
672
+ }
673
+ if (artifactRef) {
674
+ lines.push('', `Full output: /artifacts show ${artifactRef.id} --full (${artifactRef.outputBytes}B)`);
675
+ }
676
+ lines.push('', 'Preview:', preview);
677
+ return lines.join('\n');
678
+ }
259
679
  function createToolEventPresenter(events) {
260
680
  const runningToolEntries = new Map();
261
681
  return {
@@ -331,12 +751,21 @@ class AgentChatController {
331
751
  this.events = events;
332
752
  this.controllerOptions = controllerOptions;
333
753
  }
754
+ setLoopStats(stats) {
755
+ this.runtime.store.setLastLoopStats(stats);
756
+ this.events.loopStatsUpdated?.(stats);
757
+ }
334
758
  async runInput(input, options = {}) {
335
759
  const text = input.trim();
336
760
  if (!text)
337
761
  return;
338
762
  const parsed = (0, parser_1.parseInput)(text);
339
763
  if (!parsed.isCommand) {
764
+ const localFastPath = parseLocalFastPath(text);
765
+ if (localFastPath) {
766
+ await this.runLocalFastPath(text, localFastPath, options);
767
+ return;
768
+ }
340
769
  await this.runChat(text, options);
341
770
  return;
342
771
  }
@@ -400,6 +829,179 @@ class AgentChatController {
400
829
  await this.runChat(result.chatInput ?? parsed.args, options);
401
830
  }
402
831
  }
832
+ async runLocalFastPath(input, action, options = {}) {
833
+ const activeSession = this.runtime.getSession() ?? this.runtime.ensureSession() ?? (0, session_storage_1.loadSessionMeta)(this.runtime.getSession()?.id ?? '');
834
+ const sessionId = activeSession?.id;
835
+ const turnId = traceTurnId(options.turnId);
836
+ const localCallId = `local-${turnId}`;
837
+ const start = Date.now();
838
+ const preWorkspace = (0, workspace_state_1.captureWorkspaceSnapshot)(this.runtime.cwd);
839
+ const traceArgs = buildTraceArgsDetails(this.runtime.cwd, action.tool, action.args);
840
+ if (sessionId) {
841
+ (0, session_storage_1.appendSessionMessage)(sessionId, {
842
+ role: 'user',
843
+ content: input,
844
+ timestamp: Date.now(),
845
+ });
846
+ recordTraceEvent(this.events, sessionId, {
847
+ turnId,
848
+ type: 'turn_start',
849
+ inputBytes: byteLength(input),
850
+ localFastPathUsed: true,
851
+ });
852
+ appendWorkspaceSnapshotTrace(this.events, sessionId, turnId, 'pre_turn', preWorkspace);
853
+ recordTraceEvent(this.events, sessionId, {
854
+ turnId,
855
+ type: 'local_fast_path',
856
+ name: action.tool,
857
+ ...traceArgs,
858
+ note: compactMiddle(action.label, 160),
859
+ });
860
+ }
861
+ this.runtime.store.addMessage({ role: 'user', content: input });
862
+ this.events.setStatus(`Running local ${action.label}...`);
863
+ try {
864
+ const tool = (0, tools_1.getRuntimeTools)().find(candidate => candidate.name === action.tool);
865
+ const toolContext = {
866
+ cwd: this.runtime.cwd,
867
+ config: {
868
+ name: this.runtime.config.name,
869
+ mode: this.runtime.config.mode,
870
+ },
871
+ sessionId,
872
+ turnId,
873
+ };
874
+ const permission = tool?.checkPermissions?.(action.args, toolContext);
875
+ if (permission?.behavior === 'deny' || tool?.isDestructive?.(action.args) === true) {
876
+ const reason = permission?.reason || 'Local fast path blocked a destructive tool request.';
877
+ throw new LocalFastPathBlockedError(reason);
878
+ }
879
+ if (permission?.behavior === 'ask') {
880
+ throw new LocalFastPathBlockedError(permission.reason || 'Local fast path requires an allow-safe command.');
881
+ }
882
+ if (sessionId) {
883
+ recordTraceEvent(this.events, sessionId, {
884
+ turnId,
885
+ type: 'tool_call',
886
+ name: action.tool,
887
+ callId: localCallId,
888
+ ...traceArgs,
889
+ });
890
+ }
891
+ const result = await (0, tools_1.executeTool)(action.tool, action.args, options.abortSignal, {
892
+ ...toolContext,
893
+ sessionId,
894
+ turnId,
895
+ });
896
+ const duration = Date.now() - start;
897
+ const envelope = (0, tool_serializer_1.parseToolResultEnvelope)(result);
898
+ const outputBytes = typeof envelope.outputBytes === 'number'
899
+ ? envelope.outputBytes
900
+ : Buffer.byteLength(result, 'utf8');
901
+ const assistantContent = formatLocalFastPathAssistantContent(action, result, this.runtime.cwd);
902
+ const stats = (0, framework_1.createLocalFastPathLoopStats)({
903
+ finishReason: envelope.success ? 'completed' : 'failed',
904
+ toolCalls: 1,
905
+ readOnlyToolCalls: action.tool === 'exec_command' ? 0 : 1,
906
+ unsafeToolCalls: action.tool === 'exec_command' ? 1 : 0,
907
+ toolResultBytes: outputBytes,
908
+ modelVisibleToolBytes: 0,
909
+ summarizedBytes: outputBytes,
910
+ });
911
+ this.events.append({
912
+ role: envelope.success ? 'tool' : 'error',
913
+ title: 'local',
914
+ content: toolFinishContent({
915
+ type: 'tool_result',
916
+ name: action.tool,
917
+ args: action.args,
918
+ callId: localCallId,
919
+ result,
920
+ modelVisibleResult: result,
921
+ duration,
922
+ success: envelope.success,
923
+ error: envelope.error,
924
+ summary: envelope.summary,
925
+ outputBytes,
926
+ }),
927
+ });
928
+ this.runtime.store.addMessage({ role: 'assistant', content: assistantContent });
929
+ this.setLoopStats(stats);
930
+ if (sessionId) {
931
+ recordTraceEvent(this.events, sessionId, {
932
+ turnId,
933
+ type: 'tool_result',
934
+ name: action.tool,
935
+ callId: localCallId,
936
+ argsSummary: traceArgs.argsSummary,
937
+ argsArtifactId: traceArgs.argsArtifactId,
938
+ argsBytes: traceArgs.argsBytes,
939
+ success: envelope.success,
940
+ duration,
941
+ outputBytes,
942
+ modelVisibleBytes: 0,
943
+ error: envelope.error ? compactMiddle(envelope.error, 240) : undefined,
944
+ });
945
+ appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace);
946
+ recordTraceEvent(this.events, sessionId, {
947
+ turnId,
948
+ type: 'complete',
949
+ finishReason: stats.finishReason,
950
+ llmRequests: stats.llmRequests,
951
+ toolCalls: stats.toolCalls,
952
+ readOnlyToolCalls: stats.readOnlyToolCalls,
953
+ unsafeToolCalls: stats.unsafeToolCalls,
954
+ localFastPathUsed: true,
955
+ });
956
+ (0, session_storage_1.appendSessionMessage)(sessionId, {
957
+ role: 'assistant',
958
+ content: assistantContent,
959
+ timestamp: Date.now(),
960
+ });
961
+ const recordedMessages = (0, session_storage_1.readSessionMessages)(sessionId);
962
+ if (recordedMessages.length > 0) {
963
+ (0, session_storage_1.updateSessionSummary)(sessionId, recordedMessages);
964
+ }
965
+ }
966
+ this.events.setStatus(envelope.success ? `Completed local ${action.label}` : `Failed local ${action.label}`);
967
+ }
968
+ catch (error) {
969
+ const message = error instanceof Error ? error.message : String(error);
970
+ this.events.append({ role: 'error', title: 'local', content: message });
971
+ this.events.setStatus('Local command failed. Ready for the next input.');
972
+ const assistantContent = `Local fast path failed for ${action.label}.\n\n${message}`;
973
+ this.runtime.store.addMessage({ role: 'assistant', content: assistantContent });
974
+ const finishReason = error instanceof LocalFastPathBlockedError ? 'blocked' : 'failed';
975
+ const stats = (0, framework_1.createLocalFastPathLoopStats)({
976
+ finishReason,
977
+ toolCalls: 0,
978
+ });
979
+ this.setLoopStats(stats);
980
+ if (sessionId) {
981
+ recordTraceEvent(this.events, sessionId, {
982
+ turnId,
983
+ type: 'error',
984
+ name: action.tool,
985
+ error: compactTraceError(error),
986
+ });
987
+ appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace);
988
+ recordTraceEvent(this.events, sessionId, {
989
+ turnId,
990
+ type: 'complete',
991
+ finishReason: stats.finishReason,
992
+ llmRequests: stats.llmRequests,
993
+ toolCalls: stats.toolCalls,
994
+ localFastPathUsed: true,
995
+ note: 'local_fast_path_failed',
996
+ });
997
+ (0, session_storage_1.appendSessionMessage)(sessionId, {
998
+ role: 'assistant',
999
+ content: assistantContent,
1000
+ timestamp: Date.now(),
1001
+ });
1002
+ }
1003
+ }
1004
+ }
403
1005
  createCommandContext(abortSignal, turnId) {
404
1006
  return {
405
1007
  cwd: this.runtime.cwd,
@@ -414,6 +1016,9 @@ class AgentChatController {
414
1016
  this.runtime.setSession(session);
415
1017
  this.events.replaceTranscript(sessionMessagesToTranscriptEntries(session.id));
416
1018
  },
1019
+ sessionRestored: event => {
1020
+ this.events.sessionRestored?.(event);
1021
+ },
417
1022
  getSession: this.runtime.getSession,
418
1023
  abortSignal,
419
1024
  writeOutput: text => {
@@ -442,9 +1047,10 @@ class AgentChatController {
442
1047
  return;
443
1048
  }
444
1049
  const abortSignal = options.abortSignal;
445
- const turnId = options.turnId;
1050
+ const turnId = traceTurnId(options.turnId);
446
1051
  const activeSession = this.runtime.getSession() ?? this.runtime.ensureSession() ?? (0, session_storage_1.loadSessionMeta)(this.runtime.getSession()?.id ?? '');
447
1052
  const sessionId = activeSession?.id;
1053
+ const preWorkspace = (0, workspace_state_1.captureWorkspaceSnapshot)(this.runtime.cwd);
448
1054
  const runtimeTools = (0, tools_1.getRuntimeTools)();
449
1055
  const skillResolution = (0, skills_1.resolveSkillsForTurn)({
450
1056
  cwd: this.runtime.cwd,
@@ -461,6 +1067,13 @@ class AgentChatController {
461
1067
  timestamp: Date.now(),
462
1068
  appliedSkills: appliedSkillNames.length > 0 ? appliedSkillNames : undefined,
463
1069
  });
1070
+ recordTraceEvent(this.events, sessionId, {
1071
+ turnId,
1072
+ type: 'turn_start',
1073
+ inputBytes: byteLength(input),
1074
+ note: appliedSkillNames.length > 0 ? `skills=${appliedSkillNames.join(',')}` : undefined,
1075
+ });
1076
+ appendWorkspaceSnapshotTrace(this.events, sessionId, turnId, 'pre_turn', preWorkspace);
464
1077
  }
465
1078
  this.runtime.store.addMessage({ role: 'user', content: input });
466
1079
  (0, prompt_context_1.refreshProjectInstructions)(this.runtime.store, this.runtime.cwd);
@@ -497,9 +1110,14 @@ class AgentChatController {
497
1110
  let finalContent = '';
498
1111
  let finalUsage;
499
1112
  let finalModel = '';
1113
+ let pendingCompleteTrace = null;
1114
+ let pendingCompleteStats;
1115
+ const verificationResults = [];
500
1116
  const sessionMessagesToRecord = [];
501
1117
  const assistantStream = createAssistantStreamPresenter(this.events, abortSignal);
502
1118
  const toolEvents = createToolEventPresenter(this.events);
1119
+ let checkpointSequence = 0;
1120
+ const checkpointIds = [];
503
1121
  const streamCallbacks = {
504
1122
  onChunk: chunk => {
505
1123
  assistantStream.appendChunk(chunk);
@@ -524,6 +1142,14 @@ class AgentChatController {
524
1142
  turnId,
525
1143
  });
526
1144
  };
1145
+ const loopBudget = (0, loop_budget_1.resolveRuntimeLoopBudget)(input, this.runtime.config, harness.toJSON());
1146
+ let observedTurnsStarted = 0;
1147
+ let observedLlmRequests = 0;
1148
+ let observedToolCalls = 0;
1149
+ let observedReadOnlyToolCalls = 0;
1150
+ let observedUnsafeToolCalls = 0;
1151
+ let observedToolResultBytes = 0;
1152
+ let observedModelVisibleToolBytes = 0;
527
1153
  try {
528
1154
  for await (const event of (0, framework_1.query)({
529
1155
  messages,
@@ -547,11 +1173,41 @@ class AgentChatController {
547
1173
  abortSignal,
548
1174
  harness,
549
1175
  input,
1176
+ loopBudget,
550
1177
  })) {
551
1178
  switch (event.type) {
552
1179
  case 'request_start':
1180
+ observedTurnsStarted = Math.max(observedTurnsStarted, event.turn);
1181
+ observedLlmRequests++;
553
1182
  assistantStream.discardSegment();
554
1183
  this.events.setStatus((0, agent_status_1.agentStepStatus)(event.turn));
1184
+ if (sessionId) {
1185
+ recordTraceEvent(this.events, sessionId, {
1186
+ turnId,
1187
+ type: 'request_start',
1188
+ model: event.model,
1189
+ turn: event.turn,
1190
+ });
1191
+ }
1192
+ break;
1193
+ case 'prompt_assembly':
1194
+ if (sessionId) {
1195
+ recordTraceEvent(this.events, sessionId, {
1196
+ turnId,
1197
+ type: 'prompt_assembly',
1198
+ promptModelId: event.modelId,
1199
+ promptEstimatedTokens: event.estimatedTokens,
1200
+ promptBudgetTokens: event.budgetTokens,
1201
+ promptCoreTokens: event.coreTokens,
1202
+ promptEvidenceBudgetTokens: event.evidenceBudgetTokens,
1203
+ promptRecentTurnBudgetTokens: event.recentTurnBudgetTokens,
1204
+ promptSections: event.sections,
1205
+ promptIncludedEvidence: event.includedEvidence,
1206
+ promptOmittedEvidence: event.omittedEvidence,
1207
+ promptIncludedEvidenceCount: event.includedEvidenceCount,
1208
+ promptOmittedEvidenceCount: event.omittedEvidenceCount,
1209
+ });
1210
+ }
555
1211
  break;
556
1212
  case 'assistant_tool_calls':
557
1213
  assistantStream.ensureMessage(event.content || '');
@@ -559,6 +1215,21 @@ class AgentChatController {
559
1215
  if (event.toolCalls.length > 1) {
560
1216
  this.events.setStatus((0, agent_status_1.runningToolsStatus)(event.toolCalls.length));
561
1217
  }
1218
+ const checkpointId = checkpointSequence === 0
1219
+ ? turnId
1220
+ : `${turnId}-checkpoint-${checkpointSequence + 1}`;
1221
+ if (createPreToolCheckpoint(this.events, sessionId, turnId, checkpointId, this.runtime.cwd, event.toolCalls)) {
1222
+ checkpointIds.push(checkpointId);
1223
+ checkpointSequence++;
1224
+ }
1225
+ if (sessionId) {
1226
+ recordTraceEvent(this.events, sessionId, {
1227
+ turnId,
1228
+ type: 'assistant_tool_calls',
1229
+ toolCallCount: event.toolCalls.length,
1230
+ contentBytes: byteLength(event.content || ''),
1231
+ });
1232
+ }
562
1233
  sessionMessagesToRecord.push({
563
1234
  role: 'assistant',
564
1235
  content: event.content || '',
@@ -567,11 +1238,83 @@ class AgentChatController {
567
1238
  });
568
1239
  break;
569
1240
  case 'tool_call':
1241
+ observedToolCalls++;
1242
+ {
1243
+ const toolDefinition = skillResolution.tools.find(tool => tool.name === event.name);
1244
+ if (toolDefinition?.isReadOnly?.(event.args) === true) {
1245
+ observedReadOnlyToolCalls++;
1246
+ }
1247
+ else {
1248
+ observedUnsafeToolCalls++;
1249
+ }
1250
+ }
570
1251
  assistantStream.closeSegment();
571
1252
  toolEvents.start(event);
1253
+ if (sessionId) {
1254
+ const traceArgs = buildTraceArgsDetails(this.runtime.cwd, event.name, event.args);
1255
+ recordTraceEvent(this.events, sessionId, {
1256
+ turnId,
1257
+ type: 'tool_call',
1258
+ name: event.name,
1259
+ callId: event.callId,
1260
+ ...traceArgs,
1261
+ batchCount: event.batchCount,
1262
+ batchIndex: event.batchIndex,
1263
+ });
1264
+ }
1265
+ break;
1266
+ case 'permission_decision':
1267
+ if (sessionId) {
1268
+ recordTraceEvent(this.events, sessionId, {
1269
+ turnId,
1270
+ type: 'permission_decision',
1271
+ name: event.name,
1272
+ callId: event.callId,
1273
+ argsSummary: compactToolArgs(event.args),
1274
+ permissionBehavior: event.decision.behavior,
1275
+ permissionApproved: event.decision.approved,
1276
+ permissionSource: event.decision.source,
1277
+ permissionReason: event.decision.reason
1278
+ ? compactMiddle(event.decision.reason, 240)
1279
+ : undefined,
1280
+ permissionDuration: event.decision.duration,
1281
+ batchCount: event.batchCount,
1282
+ batchIndex: event.batchIndex,
1283
+ });
1284
+ }
572
1285
  break;
573
1286
  case 'tool_result': {
1287
+ observedToolResultBytes += event.outputBytes ?? byteLength(event.result);
1288
+ observedModelVisibleToolBytes += byteLength(event.modelVisibleResult);
574
1289
  toolEvents.finish(event);
1290
+ if (sessionId) {
1291
+ recordTraceEvent(this.events, sessionId, {
1292
+ turnId,
1293
+ type: 'tool_result',
1294
+ name: event.name,
1295
+ callId: event.callId,
1296
+ argsSummary: compactToolArgs(event.args),
1297
+ success: event.success,
1298
+ duration: event.duration,
1299
+ outputBytes: event.outputBytes,
1300
+ modelVisibleBytes: byteLength(event.modelVisibleResult),
1301
+ artifactId: event.artifactRef?.id,
1302
+ error: event.error ? compactMiddle(event.error, 240) : undefined,
1303
+ batchCount: event.batchCount,
1304
+ batchIndex: event.batchIndex,
1305
+ });
1306
+ }
1307
+ const verificationResult = (0, verification_profile_1.collectVerificationCommandResult)({
1308
+ toolName: event.name,
1309
+ args: event.args,
1310
+ success: event.success,
1311
+ outputBytes: event.outputBytes,
1312
+ error: event.error,
1313
+ });
1314
+ if (verificationResult) {
1315
+ verificationResults.push(verificationResult);
1316
+ appendVerificationResultTrace(this.events, sessionId, turnId, verificationResult);
1317
+ }
575
1318
  sessionMessagesToRecord.push({
576
1319
  role: 'tool',
577
1320
  content: event.result,
@@ -583,10 +1326,24 @@ class AgentChatController {
583
1326
  }
584
1327
  case 'strategy_exhausted':
585
1328
  this.events.append({ role: 'status', content: event.suggestion });
1329
+ if (sessionId) {
1330
+ recordTraceEvent(this.events, sessionId, {
1331
+ turnId,
1332
+ type: 'strategy_exhausted',
1333
+ note: compactMiddle(event.suggestion, 240),
1334
+ });
1335
+ }
586
1336
  break;
587
1337
  case 'message':
588
1338
  finalContent = event.content;
589
1339
  assistantStream.ensureMessage(event.content);
1340
+ if (sessionId) {
1341
+ recordTraceEvent(this.events, sessionId, {
1342
+ turnId,
1343
+ type: 'message',
1344
+ contentBytes: byteLength(event.content),
1345
+ });
1346
+ }
590
1347
  if (event.content) {
591
1348
  sessionMessagesToRecord.push({
592
1349
  role: 'assistant',
@@ -596,11 +1353,55 @@ class AgentChatController {
596
1353
  }
597
1354
  break;
598
1355
  case 'complete':
1356
+ if (event.content && !finalContent) {
1357
+ if (event.stats?.finishReason === 'budget_exceeded') {
1358
+ assistantStream.replaceMessage(event.content);
1359
+ }
1360
+ else {
1361
+ assistantStream.ensureMessage(event.content);
1362
+ }
1363
+ sessionMessagesToRecord.push({
1364
+ role: 'assistant',
1365
+ content: event.content,
1366
+ timestamp: Date.now(),
1367
+ });
1368
+ }
599
1369
  finalContent = event.content;
600
1370
  finalUsage = event.usage;
601
1371
  finalModel = event.model;
602
1372
  if (event.stats) {
603
- this.runtime.store.setLastLoopStats(event.stats);
1373
+ pendingCompleteStats = event.stats;
1374
+ recordProviderTraceEvents(this.events, sessionId, turnId, event.stats);
1375
+ pendingCompleteTrace = {
1376
+ turnId,
1377
+ type: 'complete',
1378
+ model: event.model,
1379
+ contentBytes: byteLength(event.content || ''),
1380
+ finishReason: event.stats.finishReason,
1381
+ llmRequests: event.stats.llmRequests,
1382
+ toolCalls: event.stats.toolCalls,
1383
+ readOnlyToolCalls: event.stats.readOnlyToolCalls,
1384
+ unsafeToolCalls: event.stats.unsafeToolCalls,
1385
+ loopBudgetSource: event.stats.loopBudgetSource,
1386
+ loopBudgetBaseProfile: event.stats.loopBudgetBaseProfile,
1387
+ loopBudgetMaxLlmRequests: event.stats.loopBudgetMaxLlmRequests,
1388
+ loopBudgetMaxToolCalls: event.stats.loopBudgetMaxToolCalls,
1389
+ loopBudgetMaxReadOnlyFragmentation: event.stats.loopBudgetMaxReadOnlyFragmentation,
1390
+ loopBudgetMaxModelVisibleBytes: event.stats.loopBudgetMaxModelVisibleBytes,
1391
+ loopBudgetConfigOverride: event.stats.loopBudgetConfigOverride,
1392
+ budgetExceededReason: event.stats.budgetExceededReason,
1393
+ continuationActions: event.stats.continuationActions,
1394
+ continuationHint: event.stats.continuationHint,
1395
+ localFastPathUsed: event.stats.localFastPathUsed,
1396
+ };
1397
+ }
1398
+ else {
1399
+ pendingCompleteTrace = {
1400
+ turnId,
1401
+ type: 'complete',
1402
+ model: event.model,
1403
+ contentBytes: byteLength(event.content || ''),
1404
+ };
604
1405
  }
605
1406
  break;
606
1407
  }
@@ -611,11 +1412,51 @@ class AgentChatController {
611
1412
  this.events.setStatus('Interrupted.');
612
1413
  removeTrailingUserMessage(this.runtime);
613
1414
  if (sessionId) {
1415
+ appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1416
+ recordTraceEvent(this.events, sessionId, {
1417
+ turnId,
1418
+ type: 'aborted',
1419
+ note: 'aborted_after_query',
1420
+ });
614
1421
  (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
615
1422
  }
616
1423
  return;
617
1424
  }
618
1425
  assistantStream.closeSegment();
1426
+ if (sessionId) {
1427
+ const { profile, summary } = appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1428
+ if (shouldRecordVerificationLoopStats(profile, summary)) {
1429
+ const stats = pendingCompleteStats ?? this.runtime.store.getSnapshot().lastLoopStats;
1430
+ if (stats) {
1431
+ pendingCompleteStats = withVerificationLoopStats(stats, summary);
1432
+ }
1433
+ }
1434
+ if ((0, verification_profile_1.shouldGateCompletion)(summary)) {
1435
+ const notice = (0, verification_profile_1.formatVerificationGateNotice)(summary);
1436
+ this.events.append({
1437
+ role: 'status',
1438
+ title: 'verification',
1439
+ content: notice,
1440
+ });
1441
+ finalContent = finalContent ? `${finalContent}\n\n${notice}` : notice;
1442
+ appendAssistantNotice(sessionMessagesToRecord, notice);
1443
+ if (pendingCompleteTrace) {
1444
+ pendingCompleteTrace.finishReason = 'completion_gate';
1445
+ pendingCompleteTrace.contentBytes = byteLength(finalContent);
1446
+ pendingCompleteTrace.note = 'verification_incomplete';
1447
+ }
1448
+ const stats = pendingCompleteStats ?? this.runtime.store.getSnapshot().lastLoopStats;
1449
+ if (stats) {
1450
+ pendingCompleteStats = {
1451
+ ...withVerificationLoopStats(stats, summary),
1452
+ finishReason: 'completion_gate',
1453
+ };
1454
+ }
1455
+ }
1456
+ }
1457
+ if (pendingCompleteStats) {
1458
+ this.setLoopStats(pendingCompleteStats);
1459
+ }
619
1460
  if (finalContent) {
620
1461
  this.runtime.store.addMessage({ role: 'assistant', content: finalContent });
621
1462
  }
@@ -633,7 +1474,11 @@ class AgentChatController {
633
1474
  });
634
1475
  const harnessState = harness.toJSON();
635
1476
  this.runtime.store.setState({ harnessState });
1477
+ emitHarnessDiagnostics(this.events, harnessState);
636
1478
  if (sessionId) {
1479
+ if (pendingCompleteTrace) {
1480
+ recordTraceEvent(this.events, sessionId, pendingCompleteTrace);
1481
+ }
637
1482
  (0, session_storage_1.updateSessionSkills)(sessionId, appliedSkillNames);
638
1483
  (0, session_storage_1.updateSessionHarnessState)(sessionId, harnessState);
639
1484
  const recordedMessages = (0, session_storage_1.readSessionMessages)(sessionId);
@@ -649,6 +1494,12 @@ class AgentChatController {
649
1494
  this.events.setStatus('Interrupted.');
650
1495
  removeTrailingUserMessage(this.runtime);
651
1496
  if (sessionId) {
1497
+ appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1498
+ recordTraceEvent(this.events, sessionId, {
1499
+ turnId,
1500
+ type: 'aborted',
1501
+ note: 'abort_error',
1502
+ });
652
1503
  (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
653
1504
  }
654
1505
  return;
@@ -656,6 +1507,60 @@ class AgentChatController {
656
1507
  assistantStream.discardSegment();
657
1508
  this.events.append({ role: 'error', content: formatChatError(error) });
658
1509
  this.events.setStatus('Turn failed. Ready for the next input.');
1510
+ const failedStats = error instanceof framework_1.QueryLoopError
1511
+ ? error.stats
1512
+ : (0, framework_1.createFailedLoopStats)({
1513
+ loopBudget,
1514
+ diagnostics: observedLlmRequests > 0 ? getLastRequestDiagnostics(this.runtime.llm) : undefined,
1515
+ turnsStarted: observedTurnsStarted,
1516
+ llmRequests: observedLlmRequests,
1517
+ toolCalls: observedToolCalls,
1518
+ readOnlyToolCalls: observedReadOnlyToolCalls,
1519
+ unsafeToolCalls: observedUnsafeToolCalls,
1520
+ toolResultBytes: observedToolResultBytes,
1521
+ modelVisibleToolBytes: observedModelVisibleToolBytes,
1522
+ });
1523
+ this.setLoopStats(failedStats);
1524
+ if (sessionId) {
1525
+ recordProviderTraceEvents(this.events, sessionId, turnId, failedStats);
1526
+ const { delta } = appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1527
+ const recoveryNotice = workspaceDeltaHasTurnChanges(delta)
1528
+ ? formatFailureRecoveryNotice(turnId, delta, checkpointIds)
1529
+ : undefined;
1530
+ if (recoveryNotice) {
1531
+ this.events.append({
1532
+ role: 'status',
1533
+ title: 'recovery',
1534
+ content: recoveryNotice,
1535
+ });
1536
+ }
1537
+ recordTraceEvent(this.events, sessionId, {
1538
+ turnId,
1539
+ type: 'error',
1540
+ error: compactTraceError(error),
1541
+ note: recoveryNotice,
1542
+ });
1543
+ recordTraceEvent(this.events, sessionId, {
1544
+ turnId,
1545
+ type: 'complete',
1546
+ model: failedStats.providerFinalModel ?? this.runtime.llm.getModel(),
1547
+ contentBytes: 0,
1548
+ finishReason: failedStats.finishReason,
1549
+ llmRequests: failedStats.llmRequests,
1550
+ toolCalls: failedStats.toolCalls,
1551
+ readOnlyToolCalls: failedStats.readOnlyToolCalls,
1552
+ unsafeToolCalls: failedStats.unsafeToolCalls,
1553
+ loopBudgetSource: failedStats.loopBudgetSource,
1554
+ loopBudgetBaseProfile: failedStats.loopBudgetBaseProfile,
1555
+ loopBudgetMaxLlmRequests: failedStats.loopBudgetMaxLlmRequests,
1556
+ loopBudgetMaxToolCalls: failedStats.loopBudgetMaxToolCalls,
1557
+ loopBudgetMaxReadOnlyFragmentation: failedStats.loopBudgetMaxReadOnlyFragmentation,
1558
+ loopBudgetMaxModelVisibleBytes: failedStats.loopBudgetMaxModelVisibleBytes,
1559
+ loopBudgetConfigOverride: failedStats.loopBudgetConfigOverride,
1560
+ localFastPathUsed: failedStats.localFastPathUsed,
1561
+ });
1562
+ (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
1563
+ }
659
1564
  const history = this.runtime.store.getSnapshot().conversationHistory;
660
1565
  if (history.length > 0) {
661
1566
  this.runtime.store.setState({ conversationHistory: history.slice(0, -1) });