maka-agent 0.2.0-dev.12.20260901 → 0.2.0-dev.14.20260902

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/THIRD_PARTY_NOTICES.txt +2 -2
  2. package/dist/acp/maka-acp-agent.js +6 -3
  3. package/dist/acp/session-registry.js +307 -0
  4. package/dist/acp/stdio-server.js +20 -1
  5. package/dist/cli-core.js +15 -2
  6. package/dist/pi-transcript.js +1 -1
  7. package/dist/pi-tui-layout.js +1 -1
  8. package/dist/pi-tui-mcp-status.js +1 -1
  9. package/dist/pi-tui-runner.js +1 -1
  10. package/dist/runtime-host-access-command.js +37 -4
  11. package/dist/runtime-host-capability-provider-command.js +16 -2
  12. package/dist/runtime-host-cli-context.js +73 -3
  13. package/dist/runtime-host-cli.js +37 -6
  14. package/dist/runtime-host-run-command.js +3 -1
  15. package/dist/runtime-host-update-command.js +59 -31
  16. package/dist/tui-copy-catalog.js +4 -2
  17. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  18. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  19. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  20. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  21. package/node_modules/@maka/core/dist/attachments.js +12 -0
  22. package/node_modules/@maka/core/dist/connection-error-copy.js +17 -34
  23. package/node_modules/@maka/core/dist/events.js +0 -13
  24. package/node_modules/@maka/core/dist/maka-wordmark.js +23 -0
  25. package/node_modules/@maka/core/dist/model-catalog.js +22 -1
  26. package/node_modules/@maka/core/dist/runtime-event.js +1 -0
  27. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +50 -1
  28. package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +9 -1
  29. package/node_modules/@maka/core/dist/runtime-policy/model-catalog-entry-codec.js +10 -1
  30. package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +75 -0
  31. package/node_modules/@maka/core/dist/runtime-policy.js +4 -2
  32. package/node_modules/@maka/core/dist/session.js +1 -0
  33. package/node_modules/@maka/core/dist/settings.js +10 -2
  34. package/node_modules/@maka/core/dist/usage-record-schema.js +11 -1
  35. package/node_modules/@maka/core/package.json +2 -1
  36. package/node_modules/@maka/mcp/dist/index.js +1 -0
  37. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +52 -80
  38. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +178 -140
  39. package/node_modules/@maka/runtime/dist/archive-read-tool.js +26 -5
  40. package/node_modules/@maka/runtime/dist/builtin-tools.js +27 -2
  41. package/node_modules/@maka/runtime/dist/context-budget-policy.js +0 -12
  42. package/node_modules/@maka/runtime/dist/conversation-copy.js +66 -43
  43. package/node_modules/@maka/runtime/dist/durable-tool-result-projection.js +31 -0
  44. package/node_modules/@maka/runtime/dist/history-compact-error.js +10 -2
  45. package/node_modules/@maka/runtime/dist/history-compact-input-fit.js +7 -1
  46. package/node_modules/@maka/runtime/dist/history-compaction.js +13 -22
  47. package/node_modules/@maka/runtime/dist/model-history.js +41 -16
  48. package/node_modules/@maka/runtime/dist/provider-image-overflow-recovery.js +12 -31
  49. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +3 -0
  50. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +13 -2
  51. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -6
  52. package/node_modules/@maka/runtime/dist/shell-tools.js +77 -1
  53. package/node_modules/@maka/runtime/dist/tool-availability.js +74 -5
  54. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +333 -52
  55. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +34 -3
  56. package/node_modules/@maka/runtime/dist/tool-runtime.js +12 -0
  57. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +0 -3
  58. package/node_modules/@maka/runtime-host/dist/client/catalog-reader.js +40 -13
  59. package/node_modules/@maka/runtime-host/dist/client/connection.js +50 -31
  60. package/node_modules/@maka/runtime-host/dist/client/host-profile.js +48 -4
  61. package/node_modules/@maka/runtime-host/dist/client/index.js +2 -2
  62. package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +32 -1
  63. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -4
  64. package/node_modules/@maka/runtime-host/dist/operator/access-management-frame.js +10 -1
  65. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +56 -12
  66. package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +3 -0
  67. package/node_modules/@maka/runtime-host/dist/protocol/configuration.js +55 -9
  68. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +37 -0
  69. package/node_modules/@maka/runtime-host/dist/protocol/index.js +17 -1
  70. package/node_modules/@maka/runtime-host/dist/protocol/network-proxy.js +1 -7
  71. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +2 -0
  72. package/node_modules/@maka/runtime-host/dist/protocol/runtime-policy.js +48 -1
  73. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +4 -2
  74. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -12
  75. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +2 -1
  76. package/node_modules/@maka/runtime-host/dist/server/agent-graph-execution-coordinator.js +1 -2
  77. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +0 -9
  78. package/node_modules/@maka/runtime-host/dist/server/configuration-coordinator.js +20 -1
  79. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +46 -24
  80. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +4 -1
  81. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +15 -0
  82. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +8 -4
  83. package/node_modules/@maka/runtime-host/dist/server/network-proxy-coordinator.js +0 -1
  84. package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +3 -0
  85. package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +28 -1
  86. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +3 -3
  87. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +35 -3
  88. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +16 -0
  89. package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +189 -6
  90. package/node_modules/@maka/storage/dist/runtime-policy-stores.js +4 -1
  91. package/node_modules/@maka/storage/dist/settings-store.js +16 -25
  92. package/node_modules/fast-uri/index.js +215 -47
  93. package/node_modules/fast-uri/lib/schemes.js +9 -4
  94. package/node_modules/fast-uri/lib/utils.js +396 -92
  95. package/node_modules/fast-uri/package.json +1 -1
  96. package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
  97. package/node_modules/qs/CHANGELOG.md +15 -0
  98. package/node_modules/qs/README.md +19 -1
  99. package/node_modules/qs/dist/qs.js +21 -21
  100. package/node_modules/qs/eslint.config.mjs +1 -1
  101. package/node_modules/qs/lib/parse.js +3 -4
  102. package/node_modules/qs/lib/stringify.js +23 -8
  103. package/node_modules/qs/lib/utils.js +10 -3
  104. package/node_modules/qs/package.json +4 -4
  105. package/package.json +1 -1
@@ -481,6 +481,9 @@ function mapBackendSessionEvent(event, ctx, memory) {
481
481
  : {}),
482
482
  ...(event.promptSegments !== undefined ? { promptSegments: event.promptSegments } : {}),
483
483
  ...(event.contextBudget !== undefined ? { contextBudget: event.contextBudget } : {}),
484
+ ...(event.lastRequestAnchor !== undefined
485
+ ? { lastRequestAnchor: event.lastRequestAnchor }
486
+ : {}),
484
487
  },
485
488
  },
486
489
  ...(event.providerRequestTraceId !== undefined
@@ -552,12 +555,6 @@ function completeRuntimeEvent(base, event, memory) {
552
555
  stateDelta.failureClass =
553
556
  memory.failureClass ?? failureClassFromCompleteStopReason(stopReason) ?? 'runtime_error';
554
557
  }
555
- // The context_budget_exhausted outcome carries which invariant made the turn
556
- // unrecoverable; the durable terminal state must not collapse it to a bare
557
- // failure class.
558
- if (event.contextBudgetExhaustedDetail !== undefined) {
559
- stateDelta.contextBudgetExhaustedDetail = event.contextBudgetExhaustedDetail;
560
- }
561
558
  if (event.contextCompactionOutcome !== undefined) {
562
559
  stateDelta.contextCompactionOutcome = event.contextCompactionOutcome;
563
560
  }
@@ -220,7 +220,78 @@ export function buildStopBackgroundTaskTool(backgroundTasks) {
220
220
  impl: ({ ref }, ctx) => backgroundTasks.stopBackgroundTask(ctx.sessionId, ref, ctx.abortSignal),
221
221
  };
222
222
  }
223
- export function buildWriteStdinTool(ptyControls) {
223
+ /** A syntactically valid PTY ref used only in the documented WriteStdin examples. */
224
+ export const WRITE_STDIN_EXAMPLE_REF = 'maka://runtime/background-tasks/sr_example';
225
+ /**
226
+ * One minimal legal payload per WriteStdin action type (plus a resize-only
227
+ * call). Each entry is valid under the loose provider schema AND passes strict
228
+ * validation after normalization — the WriteStdin contract conformance test
229
+ * asserts both, so the documented shape can never drift from what the runtime
230
+ * actually accepts. The `key (chord)` entry shows the ctrl-modified printable
231
+ * form the description points at.
232
+ */
233
+ export const WRITE_STDIN_MINIMAL_EXAMPLES = [
234
+ {
235
+ label: 'text',
236
+ payload: { ref: WRITE_STDIN_EXAMPLE_REF, actions: [{ type: 'text', text: 'hello' }] },
237
+ },
238
+ {
239
+ label: 'key (named)',
240
+ payload: { ref: WRITE_STDIN_EXAMPLE_REF, actions: [{ type: 'key', key: 'enter' }] },
241
+ },
242
+ {
243
+ label: 'key (chord)',
244
+ payload: {
245
+ ref: WRITE_STDIN_EXAMPLE_REF,
246
+ actions: [{ type: 'key', key: 'c', modifiers: ['ctrl'] }],
247
+ },
248
+ },
249
+ {
250
+ label: 'mouse click',
251
+ payload: {
252
+ ref: WRITE_STDIN_EXAMPLE_REF,
253
+ actions: [{ type: 'mouse', event: 'click', x: 0, y: 0, button: 'left' }],
254
+ },
255
+ },
256
+ {
257
+ label: 'mouse press',
258
+ payload: {
259
+ ref: WRITE_STDIN_EXAMPLE_REF,
260
+ actions: [{ type: 'mouse', event: 'press', x: 0, y: 0, button: 'left' }],
261
+ },
262
+ },
263
+ {
264
+ label: 'mouse release',
265
+ payload: {
266
+ ref: WRITE_STDIN_EXAMPLE_REF,
267
+ actions: [{ type: 'mouse', event: 'release', x: 0, y: 0, button: 'left' }],
268
+ },
269
+ },
270
+ {
271
+ label: 'mouse move',
272
+ payload: {
273
+ ref: WRITE_STDIN_EXAMPLE_REF,
274
+ actions: [{ type: 'mouse', event: 'move', x: 1, y: 1 }],
275
+ },
276
+ },
277
+ {
278
+ label: 'mouse scroll',
279
+ payload: {
280
+ ref: WRITE_STDIN_EXAMPLE_REF,
281
+ actions: [{ type: 'mouse', event: 'scroll', x: 0, y: 0, direction: 'up' }],
282
+ },
283
+ },
284
+ { label: 'resize only', payload: { ref: WRITE_STDIN_EXAMPLE_REF, size: { cols: 80, rows: 24 } } },
285
+ ];
286
+ /**
287
+ * The concrete minimal example embedded in the WriteStdin tool description,
288
+ * derived from the pinned {@link WRITE_STDIN_MINIMAL_EXAMPLES} `key (named)`
289
+ * entry so the documented shape can never drift from what conformance tests
290
+ * exercise. The example ref id is templated to `<id>` for human readers.
291
+ */
292
+ export const WRITE_STDIN_DESCRIPTION_EXAMPLE_JSON = JSON.stringify((WRITE_STDIN_MINIMAL_EXAMPLES.find((example) => example.label === 'key (named)') ??
293
+ WRITE_STDIN_MINIMAL_EXAMPLES[0]).payload).replace('sr_example', '<id>');
294
+ export function createWriteStdinSchemas() {
224
295
  const terminalAction = z.unknown().transform((value, context) => {
225
296
  try {
226
297
  return parseTerminalInputAction(value);
@@ -339,6 +410,10 @@ export function buildWriteStdinTool(ptyControls) {
339
410
  })
340
411
  .strict()
341
412
  .describe('Send ordered terminal actions and/or resize a background PTY');
413
+ return { providerParameters, strictParameters };
414
+ }
415
+ export function buildWriteStdinTool(ptyControls) {
416
+ const { providerParameters, strictParameters } = createWriteStdinSchemas();
342
417
  const providerSchema = zodSchema(providerParameters);
343
418
  const parameters = jsonSchema(async () => await providerSchema.jsonSchema, {
344
419
  validate: async (value) => {
@@ -356,6 +431,7 @@ export function buildWriteStdinTool(ptyControls) {
356
431
  `Named keys are ${TERMINAL_INPUT_NAMED_KEYS.join(', ')}. Use a printable ASCII key with ctrl or alt for chords such as Ctrl-B; use text for ordinary typing. ` +
357
432
  'Mouse coordinates are zero-based terminal cells and work only while the application has enabled SGR cell mouse reporting. ' +
358
433
  'Actions are written atomically in their listed order. Text is ordinary audited tool-call data, not a secure secret channel. ' +
434
+ `Minimal example: ${WRITE_STDIN_DESCRIPTION_EXAMPLE_JSON}. ` +
359
435
  'The returned output is the terminal state at that cut, not output attributed to this input; use Read on the ref to observe later output.',
360
436
  parameters,
361
437
  permissionArgs: (input) => parseInput(input),
@@ -45,6 +45,37 @@ const DIRECT_TOOL_NAMES = new Set([
45
45
  // Provider-routed equivalent of the direct Write/Edit surface.
46
46
  'apply_patch',
47
47
  ]);
48
+ /**
49
+ * Discovery capability-family derived from a tool's permission `categoryHint`.
50
+ *
51
+ * This reuses the existing permission taxonomy (`ToolCategory`) purely for
52
+ * *presentation* in the deferred-tool search inventory: it never loads a tool
53
+ * schema and never affects permission classification. Several permission
54
+ * categories intentionally collapse into one browsing family (e.g. every shell
55
+ * bucket → `shell`). `custom_tool` is deliberately `null` so our own
56
+ * session-scoped tools without a stronger hint keep falling back to `other`.
57
+ *
58
+ * The map is TOTAL over `ToolCategory` on purpose: adding a new category to the
59
+ * union forces an explicit decision here (family or `null`) at compile time,
60
+ * instead of silently collapsing the new category into `other`.
61
+ */
62
+ const CATEGORY_FAMILY = {
63
+ read: { id: 'filesystem', label: 'Filesystem & search' },
64
+ file_write: { id: 'filesystem', label: 'Filesystem & search' },
65
+ fs_destructive: { id: 'filesystem', label: 'Filesystem & search' },
66
+ shell_safe: { id: 'shell', label: 'Shell & processes' },
67
+ shell_unsafe: { id: 'shell', label: 'Shell & processes' },
68
+ privileged: { id: 'shell', label: 'Shell & processes' },
69
+ git_destructive: { id: 'shell', label: 'Shell & processes' },
70
+ web_read: { id: 'web', label: 'Web & network' },
71
+ network_send: { id: 'web', label: 'Web & network' },
72
+ browser: { id: 'browser', label: 'Browser automation' },
73
+ computer_use: { id: 'computer_use', label: 'Computer use' },
74
+ client_capability: { id: 'client_capability', label: 'Client capabilities' },
75
+ subagent: { id: 'agents', label: 'Agent orchestration' },
76
+ // null = intentionally ungrouped; falls back to the `other` bucket.
77
+ custom_tool: null,
78
+ };
48
79
  export function toolAvailabilityHash(config) {
49
80
  return stableHash({
50
81
  mode: config === undefined ? 'full' : 'search',
@@ -113,12 +144,50 @@ export class ToolAvailabilityRuntime {
113
144
  }
114
145
  const ungrouped = [...searchable].filter((name) => !claimed.has(name)).sort(compareExactString);
115
146
  if (ungrouped.length > 0) {
116
- const fallback = groups.find((group) => group.id === 'other');
117
- if (fallback) {
118
- fallback.toolNames = [...fallback.toolNames, ...ungrouped].sort(compareExactString);
147
+ // Bucket ungrouped native tools by their permission `categoryHint` so the
148
+ // search inventory advertises a compact capability-family map instead of a
149
+ // single opaque `other` group. This reads metadata already on the bound
150
+ // tool (no schema is loaded) and never affects permission classification.
151
+ // A caller-supplied group with a colliding id keeps precedence: family
152
+ // members merge into it. Tools with no hint (or `custom_tool`) still fall
153
+ // back to `other`.
154
+ const familyMembers = new Map();
155
+ const otherNames = [];
156
+ for (const name of ungrouped) {
157
+ const hint = this.toolsByName.get(name)?.categoryHint;
158
+ // A mapped-but-`null` entry (e.g. custom_tool) and an absent hint both
159
+ // route to `other`; only a non-null family is bucketed.
160
+ const family = hint ? CATEGORY_FAMILY[hint] : null;
161
+ if (!family) {
162
+ otherNames.push(name);
163
+ continue;
164
+ }
165
+ const bucket = familyMembers.get(family.id) ?? { label: family.label, names: [] };
166
+ bucket.names.push(name);
167
+ familyMembers.set(family.id, bucket);
119
168
  }
120
- else {
121
- groups.push({ id: 'other', toolNames: ungrouped });
169
+ for (const id of [...familyMembers.keys()].sort(compareExactString)) {
170
+ const bucket = familyMembers.get(id);
171
+ const existing = groups.find((group) => group.id === id);
172
+ if (existing) {
173
+ existing.toolNames = [...existing.toolNames, ...bucket.names].sort(compareExactString);
174
+ }
175
+ else {
176
+ groups.push({
177
+ id,
178
+ toolNames: [...bucket.names].sort(compareExactString),
179
+ ...(bucket.label !== undefined ? { label: bucket.label } : {}),
180
+ });
181
+ }
182
+ }
183
+ if (otherNames.length > 0) {
184
+ const fallback = groups.find((group) => group.id === 'other');
185
+ if (fallback) {
186
+ fallback.toolNames = [...fallback.toolNames, ...otherNames].sort(compareExactString);
187
+ }
188
+ else {
189
+ groups.push({ id: 'other', toolNames: otherNames.sort(compareExactString) });
190
+ }
122
191
  }
123
192
  }
124
193
  this.groups = groups;
@@ -25,7 +25,16 @@ export const TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS = 7_500;
25
25
  const ARCHIVE_ARTIFACT_ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
26
26
  const MAX_MANIFEST_ITEMS = 100;
27
27
  const MAX_METADATA_STRING_CHARS = 160;
28
- const MANIFEST_RESPONSE_RESERVE_CHARS = 600;
28
+ /** Upper bound on matches returned by a single search page. */
29
+ export const TOOL_RESULT_ARCHIVE_MAX_SEARCH_MATCHES = 50;
30
+ /** Characters of surrounding context kept on each side of a search match. */
31
+ const SEARCH_SNIPPET_RADIUS = 80;
32
+ /** Longest search snippet retained, so many matches still fit one response. */
33
+ const MAX_SEARCH_SNIPPET_CHARS = 240;
34
+ /** Maximum pattern length accepted by the ArchiveRead tool schema. */
35
+ const MAX_SEARCH_PATTERN_CHARS = 256;
36
+ /** Preview length surfaced by inspect for text/object payloads. */
37
+ const INSPECT_PREVIEW_CHARS = 600;
29
38
  export function buildToolResultArchiveResourceRef(input) {
30
39
  const artifactId = encodeURIComponent(input.artifactId);
31
40
  const sha256 = encodeURIComponent(input.bodySha256);
@@ -95,23 +104,59 @@ export async function readToolResultArchiveResource(reader, sessionId, request,
95
104
  const operation = request.operation ?? 'inspect';
96
105
  const limit = normalizeLimit(request.limit);
97
106
  const offset = normalizeOffset(request.offset);
107
+ const parsed = deserializeArchive(read.serializedResult);
108
+ if (operation === 'search') {
109
+ const text = resolveArchiveText(parsed, read.serializedResult);
110
+ return searchArchive(request.ref, text, request.pattern, offset);
111
+ }
98
112
  if (operation === 'read') {
99
- return pagedContent({
100
- ref: request.ref,
101
- operation,
102
- content: read.serializedResult,
103
- offset,
104
- limit,
105
- });
113
+ // Page the decoded text for string payloads so offsets land on real
114
+ // characters, not JSON escape bytes; structured payloads still page their
115
+ // canonical JSON serialization.
116
+ const text = resolveArchiveText(parsed, read.serializedResult);
117
+ if (request.unit === 'line') {
118
+ return pagedLines(request.ref, text, offset, limit);
119
+ }
120
+ return pagedContent({ ref: request.ref, operation, content: text, offset, limit });
106
121
  }
107
- const parsed = deserializeArchive(read.serializedResult);
108
122
  if (operation === 'query') {
109
123
  return queryArchiveItem(request.ref, parsed, request.itemId, offset, limit);
110
124
  }
111
- return inspectArchive(request.ref, identity, parsed);
125
+ return inspectArchive(request.ref, identity, parsed, read.serializedResult);
126
+ }
127
+ /**
128
+ * The text an archive should page/search over. String payloads decode to their
129
+ * raw content, and terminal/shell-run payloads project their stdout/stderr;
130
+ * everything else pages its canonical JSON serialization.
131
+ */
132
+ function resolveArchiveText(parsed, serializedResult) {
133
+ if (typeof parsed === 'string')
134
+ return parsed;
135
+ if (!isTerminalArchivePayload(parsed))
136
+ return serializedResult;
137
+ // Bash archives retain the canonical terminal/shell-run object. Search and
138
+ // line reads should operate on the human-readable streams, not JSON-escaped
139
+ // `\\n` sequences in that object.
140
+ const output = isRecord(parsed.output) ? parsed.output : undefined;
141
+ if (output?.mode === 'pty') {
142
+ const ptyParts = [output.scrollback, output.screen, output.lastAlternateScreen].filter((part) => typeof part === 'string' && part.length > 0);
143
+ return ptyParts.join('\n');
144
+ }
145
+ const stdout = output && typeof output.stdout === 'string' ? output.stdout : undefined;
146
+ const stderr = output && typeof output.stderr === 'string' ? output.stderr : undefined;
147
+ if (stdout !== undefined || stderr !== undefined) {
148
+ const parts = [stdout, stderr].filter((s) => typeof s === 'string' && s.length > 0);
149
+ return parts.join('\n');
150
+ }
151
+ return serializedResult;
152
+ }
153
+ function isTerminalArchivePayload(value) {
154
+ return (isRecord(value) &&
155
+ (value.kind === 'terminal' || value.kind === 'shell_run') &&
156
+ isRecord(value.output));
112
157
  }
113
158
  export const TOOL_RESULT_ARCHIVE_READ_INSTRUCTIONS = 'This result is archived but still readable. Call ArchiveRead with the provided ref and operation "inspect"; use operation "query" with itemId for one structured item, or operation "read" with offset/limit for a bounded page. Do not use Glob to find the archive.';
114
- function inspectArchive(ref, identity, value) {
159
+ function inspectArchive(ref, identity, value, serializedResult) {
115
160
  const base = {
116
161
  ok: true,
117
162
  kind: 'tool_result_archive',
@@ -120,45 +165,111 @@ function inspectArchive(ref, identity, value) {
120
165
  artifactId: identity.artifactId,
121
166
  originalBytes: identity.originalBytes,
122
167
  };
168
+ const readHint = 'Call ArchiveRead with operation "read" (unit "char" or "line"), offset, and limit for bounded pages, or operation "search" with a pattern to locate text.';
169
+ if (isTerminalArchivePayload(value)) {
170
+ const text = resolveArchiveText(value, serializedResult);
171
+ return {
172
+ ...base,
173
+ valueType: 'terminal',
174
+ totalChars: text.length,
175
+ totalLines: countLines(text),
176
+ preview: boundedString(text, INSPECT_PREVIEW_CHARS),
177
+ readHint,
178
+ };
179
+ }
180
+ if (typeof value === 'string') {
181
+ // A plain-text/terminal payload: report the char/line coordinate space plus
182
+ // a short preview so the model can page or search without a blind first read.
183
+ return {
184
+ ...base,
185
+ valueType: 'text',
186
+ totalChars: value.length,
187
+ totalLines: countLines(value),
188
+ preview: boundedString(value, INSPECT_PREVIEW_CHARS),
189
+ readHint,
190
+ };
191
+ }
123
192
  if (isRecord(value)) {
124
193
  const items = Array.isArray(value.items) ? value.items : undefined;
125
- const manifestItems = items ? fitManifestItems(base, items) : undefined;
126
- const result = {
194
+ let objectKeys = Object.keys(value)
195
+ .slice(0, 25)
196
+ .map((key) => boundedString(key));
197
+ const buildObjectBase = () => ({
127
198
  ...base,
128
199
  valueType: 'object',
129
- keys: Object.keys(value)
130
- .slice(0, 25)
131
- .map((key) => boundedString(key)),
200
+ totalChars: serializedResult.length,
201
+ keys: objectKeys,
132
202
  ...(typeof value.kind === 'string' ? { archivedKind: boundedString(value.kind) } : {}),
133
203
  ...(typeof value.status === 'string' ? { status: boundedString(value.status) } : {}),
134
- ...(items
135
- ? {
136
- itemCount: items.length,
137
- items: manifestItems,
138
- queryHint: 'Call ArchiveRead with operation "query" and one of the listed itemId values.',
204
+ });
205
+ if (items) {
206
+ const queryHint = 'Call ArchiveRead with operation "query" and one of the listed itemId values.';
207
+ const projectedItems = items.slice(0, MAX_MANIFEST_ITEMS).map(inspectItem);
208
+ let manifestItems = projectedItems;
209
+ const buildManifest = () => ({
210
+ ...buildObjectBase(),
211
+ itemCount: items.length,
212
+ items: manifestItems,
213
+ queryHint,
214
+ readHint,
215
+ ...(manifestItems.length < items.length
216
+ ? { itemsTruncated: true, listedItemCount: manifestItems.length }
217
+ : {}),
218
+ });
219
+ // Keep one actionable itemId when it can fit, then trade keys for more
220
+ // manifest entries and re-add entries against the complete envelope.
221
+ while (JSON.stringify(buildManifest()).length > TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
222
+ if (manifestItems.length > 1) {
223
+ manifestItems = manifestItems.slice(0, -1);
224
+ continue;
139
225
  }
140
- : {}),
141
- readHint: 'Call ArchiveRead with operation "read", offset, and limit for raw JSON pages.',
142
- };
143
- return {
144
- ...result,
145
- ...(items && manifestItems && manifestItems.length < items.length
146
- ? { itemsTruncated: true, listedItemCount: manifestItems.length }
147
- : {}),
148
- };
226
+ if (objectKeys.length > 0) {
227
+ objectKeys = objectKeys.slice(0, -1);
228
+ continue;
229
+ }
230
+ if (manifestItems.length > 0) {
231
+ manifestItems = [];
232
+ continue;
233
+ }
234
+ break;
235
+ }
236
+ for (const projectedItem of projectedItems.slice(manifestItems.length)) {
237
+ const next = [...manifestItems, projectedItem];
238
+ manifestItems = next;
239
+ if (JSON.stringify(buildManifest()).length > TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
240
+ manifestItems = next.slice(0, -1);
241
+ break;
242
+ }
243
+ }
244
+ return buildManifest();
245
+ }
246
+ const buildObject = () => ({
247
+ ...buildObjectBase(),
248
+ preview: boundedString(serializedResult, INSPECT_PREVIEW_CHARS),
249
+ readHint,
250
+ });
251
+ while (objectKeys.length > 0 &&
252
+ JSON.stringify(buildObject()).length > TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
253
+ objectKeys = objectKeys.slice(0, -1);
254
+ }
255
+ return buildObject();
149
256
  }
150
257
  if (Array.isArray(value)) {
151
258
  return {
152
259
  ...base,
153
260
  valueType: 'array',
154
261
  itemCount: value.length,
155
- readHint: 'Call ArchiveRead with operation "read", offset, and limit for raw JSON pages.',
262
+ totalChars: serializedResult.length,
263
+ preview: boundedString(serializedResult, INSPECT_PREVIEW_CHARS),
264
+ readHint,
156
265
  };
157
266
  }
158
267
  return {
159
268
  ...base,
160
269
  valueType: value === null ? 'null' : typeof value,
161
- readHint: 'Call ArchiveRead with operation "read", offset, and limit for content pages.',
270
+ totalChars: serializedResult.length,
271
+ preview: boundedString(serializedResult, INSPECT_PREVIEW_CHARS),
272
+ readHint,
162
273
  };
163
274
  }
164
275
  function queryArchiveItem(ref, value, itemId, offset, limit) {
@@ -225,6 +336,194 @@ function pagedContent(input) {
225
336
  }
226
337
  return buildPage(low);
227
338
  }
339
+ /**
340
+ * Locate a literal (case-insensitive) substring inside the archived text and
341
+ * return matched offsets with bounded context snippets. The pattern is escaped
342
+ * before using a Unicode-aware literal regex, so it cannot inject regex syntax
343
+ * or introduce pathological backtracking. Matches accumulate only while the
344
+ * envelope stays within
345
+ * TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS, and `nextOffset` resumes the scan.
346
+ */
347
+ function searchArchive(ref, text, pattern, offset) {
348
+ if (!pattern)
349
+ return archiveFailure(ref, 'pattern_required');
350
+ // Escape the user pattern before using a Unicode-aware literal regex. This
351
+ // preserves original source indices while retaining contextual case folding
352
+ // (for example, Greek final sigma), and remains safe from regex injection or
353
+ // pathological backtracking because the pattern is literal.
354
+ const matcher = new RegExp(escapeRegExp(pattern), 'giu');
355
+ const base = {
356
+ ok: true,
357
+ kind: 'tool_result_archive',
358
+ operation: 'search',
359
+ ref,
360
+ pattern: boundedString(pattern, MAX_SEARCH_PATTERN_CHARS),
361
+ totalChars: text.length,
362
+ };
363
+ const buildResponse = (matches, nextOffset) => ({
364
+ ...base,
365
+ matchCount: matches.length,
366
+ matches,
367
+ nextOffset,
368
+ hasMore: nextOffset !== null,
369
+ });
370
+ const matches = [];
371
+ // Line numbers are assigned by a single forward scan; match offsets only
372
+ // increase, so total work stays linear in the text length.
373
+ let scanChar = 0;
374
+ let scanLine = 1;
375
+ const lineAt = (index) => {
376
+ while (scanChar < index && scanChar < text.length) {
377
+ if (text.charCodeAt(scanChar) === 10)
378
+ scanLine++;
379
+ scanChar++;
380
+ }
381
+ return scanLine;
382
+ };
383
+ let from = normalizeSearchOffset(text, offset);
384
+ while (matches.length < TOOL_RESULT_ARCHIVE_MAX_SEARCH_MATCHES) {
385
+ matcher.lastIndex = from;
386
+ const match = matcher.exec(text);
387
+ if (!match)
388
+ return buildResponse(matches, null);
389
+ const index = match.index;
390
+ const matchEnd = index + match[0].length;
391
+ // Keep the complete match representable when the accepted pattern is
392
+ // longer than the normal snippet budget; trim surrounding context first.
393
+ const snippetLimit = Math.min(MAX_SEARCH_PATTERN_CHARS + SEARCH_SNIPPET_RADIUS * 2, Math.max(MAX_SEARCH_SNIPPET_CHARS, matchEnd - index));
394
+ const contextBudget = Math.max(0, snippetLimit - (matchEnd - index));
395
+ const before = Math.min(SEARCH_SNIPPET_RADIUS, Math.floor(contextBudget / 2));
396
+ const after = Math.min(SEARCH_SNIPPET_RADIUS, contextBudget - before);
397
+ const snippetStart = Math.max(0, index - before);
398
+ const snippetEnd = Math.min(text.length, matchEnd + after);
399
+ const candidate = {
400
+ offset: index,
401
+ line: lineAt(index),
402
+ snippetStart,
403
+ snippet: text.slice(snippetStart, snippetEnd),
404
+ };
405
+ if (JSON.stringify(buildResponse([...matches, candidate], index)).length >
406
+ TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
407
+ // This match would overflow the budget; resume here next call.
408
+ return buildResponse(matches, index);
409
+ }
410
+ matches.push(candidate);
411
+ from = index + Math.max(1, match[0].length);
412
+ }
413
+ // Hit the per-page match cap; report where an unread match still waits.
414
+ matcher.lastIndex = from;
415
+ const more = matcher.exec(text);
416
+ return buildResponse(matches, more === null ? null : more.index);
417
+ }
418
+ function escapeRegExp(value) {
419
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
420
+ }
421
+ function normalizeSearchOffset(text, offset) {
422
+ const clamped = Math.min(Math.max(0, offset), text.length);
423
+ if (clamped > 0 &&
424
+ clamped < text.length &&
425
+ isLowSurrogate(text.charCodeAt(clamped)) &&
426
+ isHighSurrogate(text.charCodeAt(clamped - 1))) {
427
+ return clamped + 1;
428
+ }
429
+ return clamped;
430
+ }
431
+ function isHighSurrogate(value) {
432
+ return value >= 0xd800 && value <= 0xdbff;
433
+ }
434
+ function isLowSurrogate(value) {
435
+ return value >= 0xdc00 && value <= 0xdfff;
436
+ }
437
+ /** Count 1-based lines in text (an empty string has zero lines). */
438
+ function countLines(text) {
439
+ if (text.length === 0)
440
+ return 0;
441
+ let count = 1;
442
+ for (let i = 0; i < text.length; i++) {
443
+ if (text.charCodeAt(i) === 10)
444
+ count++;
445
+ }
446
+ return count;
447
+ }
448
+ /**
449
+ * Collect up to `maxLines` lines starting at the zero-based `lineOffset` without
450
+ * materializing the whole text as an array — the scan stops after the window,
451
+ * so cost is bounded by the requested window, not the archive size.
452
+ */
453
+ function collectLineWindow(text, lineOffset, maxLines) {
454
+ if (text.length === 0)
455
+ return { lines: [], startChar: 0 };
456
+ let cursor = 0;
457
+ let line = 0;
458
+ while (line < lineOffset && cursor <= text.length) {
459
+ const nl = text.indexOf('\n', cursor);
460
+ if (nl === -1)
461
+ return { lines: [], startChar: text.length };
462
+ cursor = nl + 1;
463
+ line++;
464
+ }
465
+ const startChar = cursor;
466
+ const lines = [];
467
+ while (lines.length < maxLines && cursor <= text.length) {
468
+ const nl = text.indexOf('\n', cursor);
469
+ if (nl === -1) {
470
+ lines.push(text.slice(cursor));
471
+ break;
472
+ }
473
+ lines.push(text.slice(cursor, nl));
474
+ cursor = nl + 1;
475
+ if (cursor > text.length)
476
+ break;
477
+ }
478
+ return { lines, startChar };
479
+ }
480
+ /**
481
+ * Read a bounded window of whole lines. offset/limit count lines; the returned
482
+ * line count is trimmed by binary search so the response never exceeds
483
+ * TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS, and `nextLineOffset` resumes paging.
484
+ */
485
+ function pagedLines(ref, text, lineOffset, lineLimit) {
486
+ const totalLines = countLines(text);
487
+ const { lines } = collectLineWindow(text, lineOffset, lineLimit);
488
+ const buildPage = (count) => {
489
+ const endLine = lineOffset + count;
490
+ const hasMore = endLine < totalLines;
491
+ return {
492
+ ok: true,
493
+ kind: 'tool_result_archive',
494
+ operation: 'read',
495
+ ref,
496
+ unit: 'line',
497
+ lineOffset,
498
+ lineLimit: count,
499
+ totalLines,
500
+ nextLineOffset: hasMore ? endLine : null,
501
+ hasMore,
502
+ content: lines.slice(0, count).join('\n'),
503
+ };
504
+ };
505
+ let low = 0;
506
+ let high = lines.length;
507
+ while (low < high) {
508
+ const candidate = Math.ceil((low + high) / 2);
509
+ if (JSON.stringify(buildPage(candidate)).length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
510
+ low = candidate;
511
+ }
512
+ else {
513
+ high = candidate - 1;
514
+ }
515
+ }
516
+ if (low === 0 && lines.length > 0) {
517
+ return archiveFailure(ref, 'line_too_large', {
518
+ unit: 'line',
519
+ lineOffset,
520
+ totalLines,
521
+ lineChars: lines[0].length,
522
+ readHint: 'Retry with operation "read" and unit "char" to page this oversized line.',
523
+ });
524
+ }
525
+ return buildPage(low);
526
+ }
228
527
  function inspectItem(value) {
229
528
  if (!isRecord(value))
230
529
  return { valueType: value === null ? 'null' : typeof value };
@@ -264,26 +563,8 @@ function inspectItem(value) {
264
563
  ...(typeof value.result === 'string' ? { resultChars: value.result.length } : {}),
265
564
  };
266
565
  }
267
- function fitManifestItems(base, items) {
268
- const fitted = [];
269
- for (const candidate of items.slice(0, MAX_MANIFEST_ITEMS)) {
270
- const projected = inspectItem(candidate);
271
- const next = [...fitted, projected];
272
- if (JSON.stringify({
273
- ...base,
274
- items: next,
275
- }).length >
276
- TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS - MANIFEST_RESPONSE_RESERVE_CHARS) {
277
- break;
278
- }
279
- fitted.push(projected);
280
- }
281
- return fitted;
282
- }
283
- function boundedString(value) {
284
- return value.length <= MAX_METADATA_STRING_CHARS
285
- ? value
286
- : `${value.slice(0, MAX_METADATA_STRING_CHARS - 1)}…`;
566
+ function boundedString(value, max = MAX_METADATA_STRING_CHARS) {
567
+ return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
287
568
  }
288
569
  function deserializeArchive(serialized) {
289
570
  if (serialized === 'undefined')