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.
- package/THIRD_PARTY_NOTICES.txt +2 -2
- package/dist/acp/maka-acp-agent.js +6 -3
- package/dist/acp/session-registry.js +307 -0
- package/dist/acp/stdio-server.js +20 -1
- package/dist/cli-core.js +15 -2
- package/dist/pi-transcript.js +1 -1
- package/dist/pi-tui-layout.js +1 -1
- package/dist/pi-tui-mcp-status.js +1 -1
- package/dist/pi-tui-runner.js +1 -1
- package/dist/runtime-host-access-command.js +37 -4
- package/dist/runtime-host-capability-provider-command.js +16 -2
- package/dist/runtime-host-cli-context.js +73 -3
- package/dist/runtime-host-cli.js +37 -6
- package/dist/runtime-host-run-command.js +3 -1
- package/dist/runtime-host-update-command.js +59 -31
- package/dist/tui-copy-catalog.js +4 -2
- package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/node_modules/@maka/core/dist/attachments.js +12 -0
- package/node_modules/@maka/core/dist/connection-error-copy.js +17 -34
- package/node_modules/@maka/core/dist/events.js +0 -13
- package/node_modules/@maka/core/dist/maka-wordmark.js +23 -0
- package/node_modules/@maka/core/dist/model-catalog.js +22 -1
- package/node_modules/@maka/core/dist/runtime-event.js +1 -0
- package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +50 -1
- package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +9 -1
- package/node_modules/@maka/core/dist/runtime-policy/model-catalog-entry-codec.js +10 -1
- package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +75 -0
- package/node_modules/@maka/core/dist/runtime-policy.js +4 -2
- package/node_modules/@maka/core/dist/session.js +1 -0
- package/node_modules/@maka/core/dist/settings.js +10 -2
- package/node_modules/@maka/core/dist/usage-record-schema.js +11 -1
- package/node_modules/@maka/core/package.json +2 -1
- package/node_modules/@maka/mcp/dist/index.js +1 -0
- package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +52 -80
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +178 -140
- package/node_modules/@maka/runtime/dist/archive-read-tool.js +26 -5
- package/node_modules/@maka/runtime/dist/builtin-tools.js +27 -2
- package/node_modules/@maka/runtime/dist/context-budget-policy.js +0 -12
- package/node_modules/@maka/runtime/dist/conversation-copy.js +66 -43
- package/node_modules/@maka/runtime/dist/durable-tool-result-projection.js +31 -0
- package/node_modules/@maka/runtime/dist/history-compact-error.js +10 -2
- package/node_modules/@maka/runtime/dist/history-compact-input-fit.js +7 -1
- package/node_modules/@maka/runtime/dist/history-compaction.js +13 -22
- package/node_modules/@maka/runtime/dist/model-history.js +41 -16
- package/node_modules/@maka/runtime/dist/provider-image-overflow-recovery.js +12 -31
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +3 -0
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +13 -2
- package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -6
- package/node_modules/@maka/runtime/dist/shell-tools.js +77 -1
- package/node_modules/@maka/runtime/dist/tool-availability.js +74 -5
- package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +333 -52
- package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +34 -3
- package/node_modules/@maka/runtime/dist/tool-runtime.js +12 -0
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +0 -3
- package/node_modules/@maka/runtime-host/dist/client/catalog-reader.js +40 -13
- package/node_modules/@maka/runtime-host/dist/client/connection.js +50 -31
- package/node_modules/@maka/runtime-host/dist/client/host-profile.js +48 -4
- package/node_modules/@maka/runtime-host/dist/client/index.js +2 -2
- package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +32 -1
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -4
- package/node_modules/@maka/runtime-host/dist/operator/access-management-frame.js +10 -1
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +56 -12
- package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +3 -0
- package/node_modules/@maka/runtime-host/dist/protocol/configuration.js +55 -9
- package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +37 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +17 -1
- package/node_modules/@maka/runtime-host/dist/protocol/network-proxy.js +1 -7
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +2 -0
- package/node_modules/@maka/runtime-host/dist/protocol/runtime-policy.js +48 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +4 -2
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -12
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +2 -1
- package/node_modules/@maka/runtime-host/dist/server/agent-graph-execution-coordinator.js +1 -2
- package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +0 -9
- package/node_modules/@maka/runtime-host/dist/server/configuration-coordinator.js +20 -1
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +46 -24
- package/node_modules/@maka/runtime-host/dist/server/execution-service.js +4 -1
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +15 -0
- package/node_modules/@maka/runtime-host/dist/server/listener-set.js +8 -4
- package/node_modules/@maka/runtime-host/dist/server/network-proxy-coordinator.js +0 -1
- package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +28 -1
- package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +3 -3
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +35 -3
- package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +16 -0
- package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +189 -6
- package/node_modules/@maka/storage/dist/runtime-policy-stores.js +4 -1
- package/node_modules/@maka/storage/dist/settings-store.js +16 -25
- package/node_modules/fast-uri/index.js +215 -47
- package/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/fast-uri/lib/utils.js +396 -92
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
- package/node_modules/qs/CHANGELOG.md +15 -0
- package/node_modules/qs/README.md +19 -1
- package/node_modules/qs/dist/qs.js +21 -21
- package/node_modules/qs/eslint.config.mjs +1 -1
- package/node_modules/qs/lib/parse.js +3 -4
- package/node_modules/qs/lib/stringify.js +23 -8
- package/node_modules/qs/lib/utils.js +10 -3
- package/node_modules/qs/package.json +4 -4
- 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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
268
|
-
|
|
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')
|