openhorse 0.2.11 → 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 (105) hide show
  1. package/dist/commands/index.d.ts.map +1 -1
  2. package/dist/commands/index.js +681 -6
  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 +104 -1
  20. package/dist/framework/query.d.ts.map +1 -1
  21. package/dist/framework/query.js +454 -18
  22. package/dist/framework/query.js.map +1 -1
  23. package/dist/framework/store.d.ts +5 -0
  24. package/dist/framework/store.d.ts.map +1 -1
  25. package/dist/framework/store.js +4 -0
  26. package/dist/framework/store.js.map +1 -1
  27. package/dist/framework/tool-scheduler.d.ts +9 -0
  28. package/dist/framework/tool-scheduler.d.ts.map +1 -1
  29. package/dist/framework/tool-scheduler.js +34 -0
  30. package/dist/framework/tool-scheduler.js.map +1 -1
  31. package/dist/ink-ui/components/StatusLine.d.ts.map +1 -1
  32. package/dist/ink-ui/components/StatusLine.js +7 -1
  33. package/dist/ink-ui/components/StatusLine.js.map +1 -1
  34. package/dist/runtime/agent-runtime-protocol.d.ts +13 -1
  35. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  36. package/dist/runtime/agent-runtime-protocol.js +24 -0
  37. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  38. package/dist/runtime/chat-controller.d.ts +3 -0
  39. package/dist/runtime/chat-controller.d.ts.map +1 -1
  40. package/dist/runtime/chat-controller.js +930 -5
  41. package/dist/runtime/chat-controller.js.map +1 -1
  42. package/dist/runtime/loop-budget.d.ts +5 -0
  43. package/dist/runtime/loop-budget.d.ts.map +1 -0
  44. package/dist/runtime/loop-budget.js +77 -0
  45. package/dist/runtime/loop-budget.js.map +1 -0
  46. package/dist/runtime/ui-events.d.ts +39 -1
  47. package/dist/runtime/ui-events.d.ts.map +1 -1
  48. package/dist/runtime/ui-events.js.map +1 -1
  49. package/dist/services/config-dir.d.ts +2 -0
  50. package/dist/services/config-dir.d.ts.map +1 -1
  51. package/dist/services/config-dir.js +5 -0
  52. package/dist/services/config-dir.js.map +1 -1
  53. package/dist/services/config.d.ts +4 -2
  54. package/dist/services/config.d.ts.map +1 -1
  55. package/dist/services/config.js +19 -0
  56. package/dist/services/config.js.map +1 -1
  57. package/dist/services/doctor.d.ts.map +1 -1
  58. package/dist/services/doctor.js +34 -4
  59. package/dist/services/doctor.js.map +1 -1
  60. package/dist/services/global-config.d.ts +17 -0
  61. package/dist/services/global-config.d.ts.map +1 -1
  62. package/dist/services/global-config.js.map +1 -1
  63. package/dist/services/llm.d.ts +18 -0
  64. package/dist/services/llm.d.ts.map +1 -1
  65. package/dist/services/llm.js +175 -163
  66. package/dist/services/llm.js.map +1 -1
  67. package/dist/services/provider-diagnostics.d.ts +30 -0
  68. package/dist/services/provider-diagnostics.d.ts.map +1 -0
  69. package/dist/services/provider-diagnostics.js +308 -0
  70. package/dist/services/provider-diagnostics.js.map +1 -0
  71. package/dist/services/redaction.d.ts +2 -0
  72. package/dist/services/redaction.d.ts.map +1 -0
  73. package/dist/services/redaction.js +27 -0
  74. package/dist/services/redaction.js.map +1 -0
  75. package/dist/services/session-index.d.ts.map +1 -1
  76. package/dist/services/session-index.js +7 -3
  77. package/dist/services/session-index.js.map +1 -1
  78. package/dist/services/session-storage.d.ts +100 -0
  79. package/dist/services/session-storage.d.ts.map +1 -1
  80. package/dist/services/session-storage.js +154 -3
  81. package/dist/services/session-storage.js.map +1 -1
  82. package/dist/services/verification-profile.d.ts +36 -0
  83. package/dist/services/verification-profile.d.ts.map +1 -0
  84. package/dist/services/verification-profile.js +239 -0
  85. package/dist/services/verification-profile.js.map +1 -0
  86. package/dist/services/workspace-state.d.ts +25 -0
  87. package/dist/services/workspace-state.d.ts.map +1 -0
  88. package/dist/services/workspace-state.js +115 -0
  89. package/dist/services/workspace-state.js.map +1 -0
  90. package/dist/skills/loader.d.ts +5 -1
  91. package/dist/skills/loader.d.ts.map +1 -1
  92. package/dist/skills/loader.js +34 -0
  93. package/dist/skills/loader.js.map +1 -1
  94. package/dist/skills/registry.d.ts +5 -1
  95. package/dist/skills/registry.d.ts.map +1 -1
  96. package/dist/skills/registry.js +23 -0
  97. package/dist/skills/registry.js.map +1 -1
  98. package/dist/skills/runtime.d.ts +6 -0
  99. package/dist/skills/runtime.d.ts.map +1 -1
  100. package/dist/skills/runtime.js +88 -4
  101. package/dist/skills/runtime.js.map +1 -1
  102. package/dist/skills/types.d.ts +19 -0
  103. package/dist/skills/types.d.ts.map +1 -1
  104. package/dist/skills/types.js.map +1 -1
  105. 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}`;
@@ -76,6 +428,23 @@ function toolSummary(name, args, success, duration) {
76
428
  const suffix = details ? ` ${details}` : '';
77
429
  return `${success ? '✓' : '✗'} ${name}${suffix} (${duration}ms)`;
78
430
  }
431
+ function toolFinishContent(event) {
432
+ const summary = event.summary || toolSummary(event.name, event.args, event.success, event.duration);
433
+ const lines = [summary];
434
+ if (event.name === 'exec_command' && typeof event.args.command === 'string') {
435
+ lines.push(` $ ${event.args.command}`);
436
+ }
437
+ if (event.artifactRef) {
438
+ lines.push(` artifact ${event.artifactRef.id} (${event.artifactRef.outputBytes}B full output)`);
439
+ }
440
+ else if (typeof event.outputBytes === 'number') {
441
+ lines.push(` output ${event.outputBytes}B`);
442
+ }
443
+ if (event.error) {
444
+ lines.push(`Error: ${event.error}`);
445
+ }
446
+ return lines.filter(Boolean).join('\n');
447
+ }
79
448
  function isSyntheticCompactContext(content) {
80
449
  return content.startsWith('[OpenHorse Context State v2]')
81
450
  || content.startsWith('[Context Summary]')
@@ -237,8 +606,76 @@ function createAssistantStreamPresenter(events, abortSignal) {
237
606
  activeSegmentText = content;
238
607
  ensureLiveEntry();
239
608
  },
609
+ replaceMessage(content) {
610
+ if (abortSignal?.aborted || !content)
611
+ return;
612
+ activeSegmentText = content;
613
+ ensureLiveEntry();
614
+ },
240
615
  };
241
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
+ }
242
679
  function createToolEventPresenter(events) {
243
680
  const runningToolEntries = new Map();
244
681
  return {
@@ -264,11 +701,10 @@ function createToolEventPresenter(events) {
264
701
  duration: event.duration,
265
702
  summary: event.summary,
266
703
  error: event.error,
704
+ outputBytes: event.outputBytes,
705
+ artifactRef: event.artifactRef,
267
706
  });
268
- const content = [
269
- event.summary || toolSummary(event.name, event.args, event.success, event.duration),
270
- event.error ? `Error: ${event.error}` : '',
271
- ].filter(Boolean).join('\n');
707
+ const content = toolFinishContent(event);
272
708
  const existingEntryId = runningToolEntries.get(event.callId);
273
709
  if (existingEntryId) {
274
710
  events.finalize(existingEntryId, {
@@ -315,12 +751,21 @@ class AgentChatController {
315
751
  this.events = events;
316
752
  this.controllerOptions = controllerOptions;
317
753
  }
754
+ setLoopStats(stats) {
755
+ this.runtime.store.setLastLoopStats(stats);
756
+ this.events.loopStatsUpdated?.(stats);
757
+ }
318
758
  async runInput(input, options = {}) {
319
759
  const text = input.trim();
320
760
  if (!text)
321
761
  return;
322
762
  const parsed = (0, parser_1.parseInput)(text);
323
763
  if (!parsed.isCommand) {
764
+ const localFastPath = parseLocalFastPath(text);
765
+ if (localFastPath) {
766
+ await this.runLocalFastPath(text, localFastPath, options);
767
+ return;
768
+ }
324
769
  await this.runChat(text, options);
325
770
  return;
326
771
  }
@@ -384,6 +829,179 @@ class AgentChatController {
384
829
  await this.runChat(result.chatInput ?? parsed.args, options);
385
830
  }
386
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
+ }
387
1005
  createCommandContext(abortSignal, turnId) {
388
1006
  return {
389
1007
  cwd: this.runtime.cwd,
@@ -398,6 +1016,9 @@ class AgentChatController {
398
1016
  this.runtime.setSession(session);
399
1017
  this.events.replaceTranscript(sessionMessagesToTranscriptEntries(session.id));
400
1018
  },
1019
+ sessionRestored: event => {
1020
+ this.events.sessionRestored?.(event);
1021
+ },
401
1022
  getSession: this.runtime.getSession,
402
1023
  abortSignal,
403
1024
  writeOutput: text => {
@@ -426,9 +1047,10 @@ class AgentChatController {
426
1047
  return;
427
1048
  }
428
1049
  const abortSignal = options.abortSignal;
429
- const turnId = options.turnId;
1050
+ const turnId = traceTurnId(options.turnId);
430
1051
  const activeSession = this.runtime.getSession() ?? this.runtime.ensureSession() ?? (0, session_storage_1.loadSessionMeta)(this.runtime.getSession()?.id ?? '');
431
1052
  const sessionId = activeSession?.id;
1053
+ const preWorkspace = (0, workspace_state_1.captureWorkspaceSnapshot)(this.runtime.cwd);
432
1054
  const runtimeTools = (0, tools_1.getRuntimeTools)();
433
1055
  const skillResolution = (0, skills_1.resolveSkillsForTurn)({
434
1056
  cwd: this.runtime.cwd,
@@ -445,6 +1067,13 @@ class AgentChatController {
445
1067
  timestamp: Date.now(),
446
1068
  appliedSkills: appliedSkillNames.length > 0 ? appliedSkillNames : undefined,
447
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);
448
1077
  }
449
1078
  this.runtime.store.addMessage({ role: 'user', content: input });
450
1079
  (0, prompt_context_1.refreshProjectInstructions)(this.runtime.store, this.runtime.cwd);
@@ -481,9 +1110,14 @@ class AgentChatController {
481
1110
  let finalContent = '';
482
1111
  let finalUsage;
483
1112
  let finalModel = '';
1113
+ let pendingCompleteTrace = null;
1114
+ let pendingCompleteStats;
1115
+ const verificationResults = [];
484
1116
  const sessionMessagesToRecord = [];
485
1117
  const assistantStream = createAssistantStreamPresenter(this.events, abortSignal);
486
1118
  const toolEvents = createToolEventPresenter(this.events);
1119
+ let checkpointSequence = 0;
1120
+ const checkpointIds = [];
487
1121
  const streamCallbacks = {
488
1122
  onChunk: chunk => {
489
1123
  assistantStream.appendChunk(chunk);
@@ -508,6 +1142,14 @@ class AgentChatController {
508
1142
  turnId,
509
1143
  });
510
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;
511
1153
  try {
512
1154
  for await (const event of (0, framework_1.query)({
513
1155
  messages,
@@ -531,11 +1173,41 @@ class AgentChatController {
531
1173
  abortSignal,
532
1174
  harness,
533
1175
  input,
1176
+ loopBudget,
534
1177
  })) {
535
1178
  switch (event.type) {
536
1179
  case 'request_start':
1180
+ observedTurnsStarted = Math.max(observedTurnsStarted, event.turn);
1181
+ observedLlmRequests++;
537
1182
  assistantStream.discardSegment();
538
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
+ }
539
1211
  break;
540
1212
  case 'assistant_tool_calls':
541
1213
  assistantStream.ensureMessage(event.content || '');
@@ -543,6 +1215,21 @@ class AgentChatController {
543
1215
  if (event.toolCalls.length > 1) {
544
1216
  this.events.setStatus((0, agent_status_1.runningToolsStatus)(event.toolCalls.length));
545
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
+ }
546
1233
  sessionMessagesToRecord.push({
547
1234
  role: 'assistant',
548
1235
  content: event.content || '',
@@ -551,14 +1238,87 @@ class AgentChatController {
551
1238
  });
552
1239
  break;
553
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
+ }
554
1251
  assistantStream.closeSegment();
555
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
+ }
556
1285
  break;
557
1286
  case 'tool_result': {
1287
+ observedToolResultBytes += event.outputBytes ?? byteLength(event.result);
1288
+ observedModelVisibleToolBytes += byteLength(event.modelVisibleResult);
558
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
+ }
559
1318
  sessionMessagesToRecord.push({
560
1319
  role: 'tool',
561
1320
  content: event.result,
1321
+ modelVisibleContent: event.modelVisibleResult,
562
1322
  timestamp: Date.now(),
563
1323
  toolCallId: event.callId,
564
1324
  });
@@ -566,10 +1326,24 @@ class AgentChatController {
566
1326
  }
567
1327
  case 'strategy_exhausted':
568
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
+ }
569
1336
  break;
570
1337
  case 'message':
571
1338
  finalContent = event.content;
572
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
+ }
573
1347
  if (event.content) {
574
1348
  sessionMessagesToRecord.push({
575
1349
  role: 'assistant',
@@ -579,9 +1353,56 @@ class AgentChatController {
579
1353
  }
580
1354
  break;
581
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
+ }
582
1369
  finalContent = event.content;
583
1370
  finalUsage = event.usage;
584
1371
  finalModel = event.model;
1372
+ if (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
+ };
1405
+ }
585
1406
  break;
586
1407
  }
587
1408
  }
@@ -591,11 +1412,51 @@ class AgentChatController {
591
1412
  this.events.setStatus('Interrupted.');
592
1413
  removeTrailingUserMessage(this.runtime);
593
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
+ });
594
1421
  (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
595
1422
  }
596
1423
  return;
597
1424
  }
598
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
+ }
599
1460
  if (finalContent) {
600
1461
  this.runtime.store.addMessage({ role: 'assistant', content: finalContent });
601
1462
  }
@@ -613,7 +1474,11 @@ class AgentChatController {
613
1474
  });
614
1475
  const harnessState = harness.toJSON();
615
1476
  this.runtime.store.setState({ harnessState });
1477
+ emitHarnessDiagnostics(this.events, harnessState);
616
1478
  if (sessionId) {
1479
+ if (pendingCompleteTrace) {
1480
+ recordTraceEvent(this.events, sessionId, pendingCompleteTrace);
1481
+ }
617
1482
  (0, session_storage_1.updateSessionSkills)(sessionId, appliedSkillNames);
618
1483
  (0, session_storage_1.updateSessionHarnessState)(sessionId, harnessState);
619
1484
  const recordedMessages = (0, session_storage_1.readSessionMessages)(sessionId);
@@ -629,6 +1494,12 @@ class AgentChatController {
629
1494
  this.events.setStatus('Interrupted.');
630
1495
  removeTrailingUserMessage(this.runtime);
631
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
+ });
632
1503
  (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
633
1504
  }
634
1505
  return;
@@ -636,6 +1507,60 @@ class AgentChatController {
636
1507
  assistantStream.discardSegment();
637
1508
  this.events.append({ role: 'error', content: formatChatError(error) });
638
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
+ }
639
1564
  const history = this.runtime.store.getSnapshot().conversationHistory;
640
1565
  if (history.length > 0) {
641
1566
  this.runtime.store.setState({ conversationHistory: history.slice(0, -1) });