maka-agent 0.2.0-dev.24.20260907 → 0.2.0-dev.26.20260909
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -19
- package/README.zh-CN.md +34 -19
- package/dist/mcp-capability-provider.js +4 -1
- package/dist/pi-transcript-tools.js +6 -0
- package/dist/pi-tui-layout.js +56 -10
- package/dist/pi-tui-mcp-status.js +2 -2
- package/dist/pi-tui-pickers.js +121 -30
- package/dist/pi-tui-runner.js +156 -42
- package/dist/pi-tui-todo.js +221 -0
- package/dist/pi-tui-transcript-viewer.js +134 -7
- package/dist/runtime-host-cli-context.js +70 -13
- package/dist/runtime-host-handoff-surface.js +51 -26
- package/dist/runtime-host-local-handoff.js +11 -1
- package/dist/runtime-host-service-manager.js +0 -3
- package/dist/runtime-host-session-channel.js +13 -0
- package/dist/runtime-host-session-driver.js +42 -1
- package/dist/tui-autocomplete-layout.js +3 -1
- package/dist/tui-copy-catalog.js +85 -6
- package/dist/tui-editor-render.js +36 -0
- package/dist/tui-mcp-control.js +41 -6
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/agent-run.js +1 -0
- package/node_modules/@maka/core/dist/attachments.js +3 -0
- package/node_modules/@maka/core/dist/canonical-json.js +27 -0
- package/node_modules/@maka/core/dist/capabilities.js +35 -0
- package/node_modules/@maka/core/dist/computer-use.js +0 -1
- package/node_modules/@maka/core/dist/display-redaction.js +18 -0
- package/node_modules/@maka/core/dist/llm-connections.js +13 -0
- package/node_modules/@maka/core/dist/model-call-attempt.js +2 -0
- package/node_modules/@maka/core/dist/model-metadata.js +4 -0
- package/node_modules/@maka/core/dist/redaction.js +35 -0
- package/node_modules/@maka/core/dist/run-composition.js +77 -2
- package/node_modules/@maka/core/dist/runtime-event.js +3 -0
- package/node_modules/@maka/core/dist/session.js +57 -6
- package/node_modules/@maka/core/dist/slash-command-catalog.js +1 -0
- package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
- package/node_modules/@maka/core/dist/workhub-action-result.js +68 -0
- package/node_modules/@maka/core/package.json +3 -3
- package/node_modules/@maka/eval/dist/experiment-directory.js +4 -13
- package/node_modules/@maka/eval/dist/toolchain-verification.js +1 -1
- package/node_modules/@maka/mcp/dist/form-elicitation.js +213 -0
- package/node_modules/@maka/mcp/dist/index.js +337 -71
- package/node_modules/@maka/runtime/dist/agent-run-inspect.js +0 -13
- package/node_modules/@maka/runtime/dist/agent-run.js +90 -2
- package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +16 -8
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +57 -50
- package/node_modules/@maka/runtime/dist/archive-read-tool.js +2 -2
- package/node_modules/@maka/runtime/dist/context-budget-helpers.js +0 -16
- package/node_modules/@maka/runtime/dist/conversation-copy.js +153 -11
- package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +3 -3
- package/node_modules/@maka/runtime/dist/goal-continuation.js +5 -1
- package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +69 -1
- package/node_modules/@maka/runtime/dist/mcp-tools.js +7 -2
- package/node_modules/@maka/runtime/dist/model-fetcher.js +42 -2
- package/node_modules/@maka/runtime/dist/model-history.js +0 -112
- package/node_modules/@maka/runtime/dist/plugin-runtime.js +20 -10
- package/node_modules/@maka/runtime/dist/plugin-tool-service.js +205 -0
- package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +8 -1
- package/node_modules/@maka/runtime/dist/request-shape.js +23 -0
- package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -1
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +24 -1
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +127 -13
- package/node_modules/@maka/runtime/dist/session-manager.js +20 -36
- package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +21 -0
- package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +0 -5
- package/node_modules/@maka/runtime/dist/test-connection.js +0 -1
- package/node_modules/@maka/runtime/dist/tool-activation-identity.js +41 -0
- package/node_modules/@maka/runtime/dist/tool-availability.js +11 -1
- package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +63 -1
- package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +42 -12
- package/node_modules/@maka/runtime/dist/tool-result-archive.js +27 -4
- package/node_modules/@maka/runtime/package.json +1 -0
- package/node_modules/@maka/runtime-host/dist/client/connection.js +30 -5
- package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +52 -13
- package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +2 -0
- package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +4 -2
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +18 -1
- package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +19 -0
- package/node_modules/@maka/runtime-host/dist/control/endpoint.js +1 -3
- package/node_modules/@maka/runtime-host/dist/node-error.js +22 -0
- package/node_modules/@maka/runtime-host/dist/operator/managed-deployment.js +1 -3
- package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +0 -1
- package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +1 -1
- package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +1 -3
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +3 -2
- package/node_modules/@maka/runtime-host/dist/protocol/plan.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +27 -5
- package/node_modules/@maka/runtime-host/dist/protocol/project-catalog.js +2 -2
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +86 -157
- package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/agent-graph-coordinator.js +0 -3
- package/node_modules/@maka/runtime-host/dist/server/canonical-session-projection.js +2 -1
- package/node_modules/@maka/runtime-host/dist/server/connection-session.js +28 -8
- package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +112 -56
- package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +32 -32
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +85 -25
- package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +42 -14
- package/node_modules/@maka/runtime-host/dist/server/failure-diagnostic.js +24 -0
- package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +7 -3
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -3
- package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +5 -2
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +3 -2
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +34 -4
- package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +51 -23
- package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +2 -7
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +110 -14
- package/node_modules/@maka/runtime-host/dist/server/runtime-resource-coordinator.js +11 -8
- package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +31 -21
- package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +20 -8
- package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +5 -2
- package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +15 -104
- package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +59 -82
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +132 -139
- package/node_modules/@maka/runtime-host/dist/server/workhub-message-attachments.js +46 -0
- package/node_modules/@maka/storage/dist/agent-run-store.js +14 -1
- package/node_modules/@maka/storage/dist/artifact-store.js +40 -3
- package/node_modules/@maka/storage/dist/atomic-file-write.js +74 -0
- package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -1
- package/node_modules/@maka/storage/dist/codex-session-adapter.js +284 -196
- package/node_modules/@maka/storage/dist/credential-store.js +12 -63
- package/node_modules/@maka/storage/dist/execution-stores.js +4 -1
- package/node_modules/@maka/storage/dist/git-exec.js +61 -0
- package/node_modules/@maka/storage/dist/git-worktree-child-executor.js +8 -35
- package/node_modules/@maka/storage/dist/goal-authority.js +2 -2
- package/node_modules/@maka/storage/dist/interaction-store.js +2 -2
- package/node_modules/@maka/storage/dist/mcp-config-store.js +10 -25
- package/node_modules/@maka/storage/dist/project-catalog.js +2 -23
- package/node_modules/@maka/storage/dist/runtime-event-invariants.js +2 -2
- package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +8 -3
- package/node_modules/@maka/storage/dist/scheduled-task-store.js +249 -283
- package/node_modules/@maka/storage/dist/session-bundle-manifest.js +5 -17
- package/node_modules/@maka/storage/dist/session-bundle-policy.js +2 -1
- package/node_modules/@maka/storage/dist/session-copy-cleanup.js +2 -1
- package/node_modules/@maka/storage/dist/session-store.js +14 -2
- package/node_modules/@maka/storage/dist/settings-store.js +8 -4
- package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +11 -0
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +1 -2
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +12 -1
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +67 -3
- package/node_modules/@maka/storage/dist/stable-storage.js +14 -1
- package/node_modules/@maka/storage/dist/storage-id.js +28 -0
- package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +31 -4
- package/node_modules/@maka/storage/dist/workspace-identity.js +2 -15
- package/node_modules/@maka/storage/dist/workspace-version-authority-internal.js +1 -8
- package/package.json +1 -1
- package/node_modules/@maka/core/dist/workhub-creation-intent.js +0 -990
- package/node_modules/@maka/core/dist/workhub-session-resolver.js +0 -50
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
19
|
import { randomBytes } from 'node:crypto';
|
|
20
|
-
import { auth, Client, extractWWWAuthenticateParams, LATEST_PROTOCOL_VERSION, SdkErrorCode, SdkHttpError, SSEClientTransport, StreamableHTTPClientTransport, } from '@modelcontextprotocol/client';
|
|
20
|
+
import { auth, Client, extractWWWAuthenticateParams, LATEST_PROTOCOL_VERSION, CLIENT_CAPABILITIES_META_KEY, isInputRequiredResult, SdkErrorCode, SdkHttpError, SSEClientTransport, StreamableHTTPClientTransport, } from '@modelcontextprotocol/client';
|
|
21
21
|
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
|
22
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
23
|
+
import { containsMcpFormState, prepareMcpForm } from './form-elicitation.js';
|
|
22
24
|
import { redactSecrets } from '@maka/core/redaction';
|
|
23
25
|
import { serializedByteLength } from '@maka/core/serialized-byte-length';
|
|
24
26
|
import { deepScrubMcpSecrets, EMPTY_MCP_SECRET_INVENTORY, MCP_SECRET_MIN_SUBSTITUTION_LENGTH, mcpSecretInventory, scrubKnownMcpSecrets, } from '@maka/core/mcp-secrets';
|
|
@@ -59,6 +61,10 @@ const TOOL_REFRESH_BURST_IDLE_MS = 1_000;
|
|
|
59
61
|
// access/refresh/id token, client secret, verifier), small enough that
|
|
60
62
|
// deepScrub stays O(bound) per payload for the life of the process.
|
|
61
63
|
const MAX_HARVESTED_SECRETS_PER_SERVER = 40;
|
|
64
|
+
// No eviction: a live transport may reflect any previous continuation later.
|
|
65
|
+
const MAX_CONNECTION_FORM_STATES = 64;
|
|
66
|
+
const MAX_CONNECTION_FORM_STATE_BYTES = 256 * 1024;
|
|
67
|
+
const FORM_PRIVACY_EXHAUSTED = 'form privacy retention exhausted (state limit); reconnect required';
|
|
62
68
|
export class McpClientManager {
|
|
63
69
|
connections = new Map();
|
|
64
70
|
/** Servers with an interactive OAuth round in flight (startAuthorization
|
|
@@ -67,6 +73,7 @@ export class McpClientManager {
|
|
|
67
73
|
* pass — and racing one against the round trips the round's version
|
|
68
74
|
* fence, failing the user's login over nothing they did. */
|
|
69
75
|
interactiveRounds = new Set();
|
|
76
|
+
formCalls = new Map();
|
|
70
77
|
bindingIndex = new Map();
|
|
71
78
|
listeners = new Set();
|
|
72
79
|
syncQueue = Promise.resolve();
|
|
@@ -174,6 +181,22 @@ export class McpClientManager {
|
|
|
174
181
|
for (const value of this.storedSecrets.get(serverId) ?? []) {
|
|
175
182
|
(value.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(value);
|
|
176
183
|
}
|
|
184
|
+
const privacy = this.connections.get(serverId)?.diagnosticPrivacy;
|
|
185
|
+
if (privacy?.exhausted) {
|
|
186
|
+
// An empty withheld token intentionally matches every diagnostic. Once
|
|
187
|
+
// retention is full, fail closed until a fresh physical connection.
|
|
188
|
+
inventory.withhold.push('');
|
|
189
|
+
}
|
|
190
|
+
for (const state of privacy?.states ?? []) {
|
|
191
|
+
(state.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(state);
|
|
192
|
+
}
|
|
193
|
+
for (const call of this.formCalls.values()) {
|
|
194
|
+
if (call.serverId !== serverId)
|
|
195
|
+
continue;
|
|
196
|
+
for (const state of call.states) {
|
|
197
|
+
(state.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(state);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
177
200
|
return inventory;
|
|
178
201
|
}
|
|
179
202
|
onChange(listener) {
|
|
@@ -520,93 +543,231 @@ export class McpClientManager {
|
|
|
520
543
|
throw new McpToolCallError(serverId, toolName, 'tool binding is stale');
|
|
521
544
|
}
|
|
522
545
|
const client = entry.client;
|
|
546
|
+
let inventory = this.secretsFor(serverId, entry.config);
|
|
523
547
|
const headerArguments = validateMcpHeaderArguments(snapshot.headerDeclarations, args);
|
|
524
548
|
if (!headerArguments.valid) {
|
|
525
|
-
throw new McpToolCallError(serverId, toolName, `unsafe integer argument at ${formatMcpDiagnosticText(headerArguments.path.join('.'))}`);
|
|
549
|
+
throw new McpToolCallError(serverId, toolName, `unsafe integer argument at ${formatMcpDiagnosticText(scrubKnownSecrets(headerArguments.path.join('.'), inventory))}`);
|
|
526
550
|
}
|
|
527
|
-
const inventory = this.secretsFor(serverId, entry.config);
|
|
528
551
|
const preparation = snapshot.callPreparation ??
|
|
529
552
|
(snapshot.callPreparation = this.toolCallPreparer.prepare(snapshot.definition));
|
|
530
553
|
if (!preparation.ok) {
|
|
531
554
|
throw new McpToolCallError(serverId, toolName, 'server advertised an invalid output schema', {
|
|
532
|
-
cause: preparation.cause,
|
|
555
|
+
cause: sanitizedCause(preparation.cause, inventory),
|
|
533
556
|
});
|
|
534
557
|
}
|
|
535
|
-
|
|
558
|
+
const requestInteraction = entry.status.negotiatedProtocol?.era === 'modern' ? options.requestInteraction : undefined;
|
|
559
|
+
const privacy = entry.diagnosticPrivacy;
|
|
560
|
+
if (privacy?.exhausted) {
|
|
561
|
+
throw new McpToolCallError(serverId, toolName, FORM_PRIVACY_EXHAUSTED);
|
|
562
|
+
}
|
|
563
|
+
const controller = requestInteraction ? new AbortController() : undefined;
|
|
564
|
+
const signal = controller
|
|
565
|
+
? AbortSignal.any([
|
|
566
|
+
controller.signal,
|
|
567
|
+
...(privacy ? [privacy.failure.signal] : []),
|
|
568
|
+
...(options.signal ? [options.signal] : []),
|
|
569
|
+
])
|
|
570
|
+
: privacy
|
|
571
|
+
? AbortSignal.any([privacy.failure.signal, ...(options.signal ? [options.signal] : [])])
|
|
572
|
+
: options.signal;
|
|
573
|
+
const refreshInventory = () => {
|
|
574
|
+
inventory = this.secretsFor(serverId, entry.config);
|
|
575
|
+
if (privacy?.exhausted)
|
|
576
|
+
inventory.withhold.push('');
|
|
577
|
+
for (const state of privacy?.states ?? []) {
|
|
578
|
+
(state.length >= MIN_SUBSTITUTION_LENGTH ? inventory.substitute : inventory.withhold).push(state);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const states = [];
|
|
582
|
+
if (controller) {
|
|
583
|
+
this.formCalls.set(controller, { binding, serverId, states });
|
|
584
|
+
if (privacy) {
|
|
585
|
+
privacy.pendingCalls += 1;
|
|
586
|
+
privacy.discardPendingStderr?.();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const assertCurrent = () => {
|
|
590
|
+
signal?.throwIfAborted();
|
|
591
|
+
if (this.closed ||
|
|
592
|
+
entry.closing ||
|
|
593
|
+
this.connections.get(serverId) !== entry ||
|
|
594
|
+
entry.client !== client ||
|
|
595
|
+
entry.status.state !== 'connected' ||
|
|
596
|
+
entry.connectionGeneration !== identity.connectionGeneration ||
|
|
597
|
+
entry.toolSnapshot.get(toolName)?.binding !== binding)
|
|
598
|
+
throw new McpToolCallError(serverId, toolName, 'tool binding is stale');
|
|
599
|
+
};
|
|
536
600
|
try {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
601
|
+
const originalArguments = requestInteraction ? structuredClone(args) : args;
|
|
602
|
+
let continuation = {};
|
|
603
|
+
let rounds = 0;
|
|
604
|
+
let result;
|
|
605
|
+
while (true) {
|
|
606
|
+
if (requestInteraction)
|
|
607
|
+
assertCurrent();
|
|
608
|
+
result = await client.callTool({
|
|
609
|
+
name: toolName,
|
|
610
|
+
arguments: originalArguments,
|
|
611
|
+
...continuation,
|
|
612
|
+
...(requestInteraction
|
|
613
|
+
? {
|
|
614
|
+
_meta: { [CLIENT_CAPABILITIES_META_KEY]: { elicitation: { form: {} } } },
|
|
615
|
+
}
|
|
616
|
+
: {}),
|
|
617
|
+
}, {
|
|
618
|
+
signal,
|
|
619
|
+
timeout: options.timeoutMs ?? this.timeouts.callToolMs,
|
|
620
|
+
toolDefinition: structuredClone(preparation.value.definitionForSdk),
|
|
621
|
+
...(requestInteraction ? { allowInputRequired: true } : {}),
|
|
622
|
+
});
|
|
623
|
+
refreshInventory();
|
|
624
|
+
if (privacy?.exhausted)
|
|
625
|
+
throw new McpToolCallError(serverId, toolName, FORM_PRIVACY_EXHAUSTED);
|
|
626
|
+
if (requestInteraction)
|
|
627
|
+
assertCurrent();
|
|
628
|
+
if (!requestInteraction || !isInputRequiredResult(result))
|
|
629
|
+
break;
|
|
630
|
+
const state = result.requestState;
|
|
631
|
+
if (state !== undefined &&
|
|
632
|
+
(typeof state !== 'string' || Buffer.byteLength(state) > 16 * 1024)) {
|
|
633
|
+
if (privacy) {
|
|
634
|
+
exhaustConnectionDiagnosticPrivacy(entry, privacy);
|
|
635
|
+
this.emit(entry.status);
|
|
636
|
+
}
|
|
637
|
+
inventory.withhold.push('');
|
|
638
|
+
throw new McpToolCallError(serverId, toolName, 'form continuation state exceeds the limit');
|
|
639
|
+
}
|
|
640
|
+
const learnedState = Boolean(state && privacy && !privacy.states.has(state));
|
|
641
|
+
if (state) {
|
|
642
|
+
if (privacy && !retainConnectionFormState(entry, privacy, state)) {
|
|
643
|
+
this.emit(entry.status);
|
|
644
|
+
inventory.withhold.push('');
|
|
645
|
+
throw new McpToolCallError(serverId, toolName, FORM_PRIVACY_EXHAUSTED);
|
|
646
|
+
}
|
|
647
|
+
states.push(state);
|
|
648
|
+
(state.length >= MIN_SUBSTITUTION_LENGTH
|
|
649
|
+
? inventory.substitute
|
|
650
|
+
: inventory.withhold).push(state);
|
|
651
|
+
}
|
|
652
|
+
if (learnedState && entry.status.stderrTail !== undefined) {
|
|
653
|
+
// Existing lines have already been formatted/truncated, so an exact
|
|
654
|
+
// replacement cannot reliably remove fragments of newly learned
|
|
655
|
+
// state. Clear the cache and notify consumers. Earlier published
|
|
656
|
+
// diagnostics from before the state was known cannot be retracted.
|
|
657
|
+
entry.status = { ...entry.status, stderrTail: undefined };
|
|
658
|
+
this.emit(entry.status);
|
|
659
|
+
}
|
|
660
|
+
if (++rounds > 8)
|
|
661
|
+
throw new McpToolCallError(serverId, toolName, 'form round limit exceeded');
|
|
662
|
+
const requests = result.inputRequests ?? {};
|
|
663
|
+
const keys = Object.keys(requests).sort(compareText);
|
|
664
|
+
if (keys.length > 8 || serializedByteLength(requests, 64 * 1024) > 64 * 1024) {
|
|
665
|
+
throw new McpToolCallError(serverId, toolName, 'form input requests exceed the limit');
|
|
666
|
+
}
|
|
667
|
+
if (containsMcpFormState(requests, privacy ? [...privacy.states] : states)) {
|
|
668
|
+
throw new McpToolCallError(serverId, toolName, 'form contains private continuation material');
|
|
669
|
+
}
|
|
670
|
+
// Validate every sibling before publishing any user-facing Interaction.
|
|
671
|
+
const forms = keys.map((key) => ({
|
|
672
|
+
key,
|
|
673
|
+
prepared: prepareMcpForm(requests[key], { name: toolName, source: serverId }),
|
|
674
|
+
}));
|
|
675
|
+
for (const { prepared } of forms) {
|
|
676
|
+
if (JSON.stringify(deepScrub(prepared.form, inventory)) !== JSON.stringify(prepared.form)) {
|
|
677
|
+
throw new McpToolCallError(serverId, toolName, 'form contains private continuation material');
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const responses = {};
|
|
681
|
+
for (const { key, prepared } of forms) {
|
|
682
|
+
assertCurrent();
|
|
683
|
+
// Providers may ignore cancellationSignal. Racing the callback still
|
|
684
|
+
// settles their invocation, which closes the canonical Host form.
|
|
685
|
+
const answer = await waitForMcpForm(() => requestInteraction(prepared.form, { cancellationSignal: signal }), signal);
|
|
686
|
+
assertCurrent();
|
|
687
|
+
Object.defineProperty(responses, key, {
|
|
688
|
+
value: prepared.respond(answer),
|
|
689
|
+
enumerable: true,
|
|
690
|
+
configurable: true,
|
|
691
|
+
writable: true,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
continuation = {
|
|
695
|
+
...(keys.length ? { inputResponses: responses } : {}),
|
|
696
|
+
...(state === undefined ? {} : { requestState: state }),
|
|
697
|
+
};
|
|
698
|
+
if (!keys.length)
|
|
699
|
+
await delay(25, undefined, { signal });
|
|
700
|
+
}
|
|
701
|
+
// The SDK's legacy compatibility schema defaults a missing content array
|
|
702
|
+
// before returning, but retains deferred-result compatibility fields.
|
|
703
|
+
// Reject every decoded marker and any future content-less result.
|
|
704
|
+
if (!Object.hasOwn(result, 'content') ||
|
|
705
|
+
Object.hasOwn(result, 'toolResult') ||
|
|
706
|
+
Object.hasOwn(result, 'task') ||
|
|
707
|
+
Object.hasOwn(result, 'inputRequests') ||
|
|
708
|
+
Object.hasOwn(result, 'requestState')) {
|
|
709
|
+
throw new McpToolCallError(serverId, toolName, 'server returned an unsupported deferred tool result');
|
|
710
|
+
}
|
|
711
|
+
if (!Array.isArray(result.content)) {
|
|
712
|
+
throw new McpToolCallError(serverId, toolName, 'server returned invalid content');
|
|
713
|
+
}
|
|
714
|
+
if (result.isError) {
|
|
715
|
+
// The server writes this text; it can echo a secret it was sent.
|
|
716
|
+
throw new McpToolCallError(serverId, toolName, scrubKnownSecrets(redactSecrets(summarizeErrorContent(result.content)), inventory));
|
|
717
|
+
}
|
|
718
|
+
assertSuccessfulToolResultBudget(serverId, toolName, result, { raw: true });
|
|
719
|
+
const validateOutput = preparation.value.validateOutput;
|
|
720
|
+
if (validateOutput) {
|
|
721
|
+
if (result.structuredContent === undefined) {
|
|
722
|
+
throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result');
|
|
723
|
+
}
|
|
724
|
+
let validation;
|
|
725
|
+
try {
|
|
726
|
+
validation = validateOutput(result.structuredContent);
|
|
727
|
+
}
|
|
728
|
+
catch (cause) {
|
|
729
|
+
throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
|
|
730
|
+
cause,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
if (!validation.valid) {
|
|
734
|
+
throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
|
|
735
|
+
cause: new Error(validation.errorMessage),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Success payloads cross toward the renderer and the transcript too; a
|
|
740
|
+
// server can embed the credential it was just sent into a result.
|
|
741
|
+
const published = {
|
|
742
|
+
content: deepScrub(result.content.map(normalizeContent), inventory),
|
|
743
|
+
structuredContent: deepScrub(result.structuredContent, inventory),
|
|
744
|
+
};
|
|
745
|
+
assertSuccessfulToolResultBudget(serverId, toolName, published);
|
|
746
|
+
return published;
|
|
542
747
|
}
|
|
543
748
|
catch (error) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
// login it now needs.
|
|
749
|
+
refreshInventory();
|
|
750
|
+
const normalized = normalizeToolCallError(serverId, toolName, error, signal);
|
|
547
751
|
if (isAuthRequiredError(error) &&
|
|
548
752
|
this.connections.get(serverId) === entry &&
|
|
549
753
|
entry.client === client) {
|
|
550
|
-
this.markError(entry, error);
|
|
754
|
+
this.markError(entry, scrubbedError(error, inventory));
|
|
551
755
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
// leaves the manager (IPC, logs, telemetry), and any cause-aware
|
|
556
|
-
// serializer downstream would expose it — so the retained cause is an
|
|
557
|
-
// allowlisted copy: typed identity (name, code, status, SDK brands)
|
|
558
|
-
// with a scrubbed message and no deeper chain or payload fields.
|
|
559
|
-
const normalized = normalizeToolCallError(serverId, toolName, error, options.signal);
|
|
560
|
-
normalized.message = scrubKnownSecrets(normalized.message, inventory);
|
|
756
|
+
normalized.message = privacy?.exhausted
|
|
757
|
+
? FORM_PRIVACY_EXHAUSTED
|
|
758
|
+
: scrubKnownSecrets(normalized.message, inventory);
|
|
561
759
|
normalized.cause = sanitizedCause(normalized.cause, inventory);
|
|
562
760
|
throw normalized;
|
|
563
761
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
Object.hasOwn(result, 'requestState')) {
|
|
572
|
-
throw new McpToolCallError(serverId, toolName, 'server returned an unsupported deferred tool result');
|
|
573
|
-
}
|
|
574
|
-
if (!Array.isArray(result.content)) {
|
|
575
|
-
throw new McpToolCallError(serverId, toolName, 'server returned invalid content');
|
|
576
|
-
}
|
|
577
|
-
if (result.isError) {
|
|
578
|
-
// The server writes this text; it can echo a secret it was sent.
|
|
579
|
-
throw new McpToolCallError(serverId, toolName, scrubKnownSecrets(redactSecrets(summarizeErrorContent(result.content)), inventory));
|
|
580
|
-
}
|
|
581
|
-
assertSuccessfulToolResultBudget(serverId, toolName, result, { raw: true });
|
|
582
|
-
const validateOutput = preparation.value.validateOutput;
|
|
583
|
-
if (validateOutput) {
|
|
584
|
-
if (result.structuredContent === undefined) {
|
|
585
|
-
throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result');
|
|
586
|
-
}
|
|
587
|
-
let validation;
|
|
588
|
-
try {
|
|
589
|
-
validation = validateOutput(result.structuredContent);
|
|
590
|
-
}
|
|
591
|
-
catch (cause) {
|
|
592
|
-
throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
|
|
593
|
-
cause,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
if (!validation.valid) {
|
|
597
|
-
throw new McpToolCallError(serverId, toolName, 'server returned an invalid tool result', {
|
|
598
|
-
cause: new Error(validation.errorMessage),
|
|
599
|
-
});
|
|
762
|
+
finally {
|
|
763
|
+
if (controller) {
|
|
764
|
+
this.formCalls.delete(controller);
|
|
765
|
+
if (privacy) {
|
|
766
|
+
privacy.discardPendingStderr?.();
|
|
767
|
+
privacy.pendingCalls -= 1;
|
|
768
|
+
}
|
|
600
769
|
}
|
|
601
770
|
}
|
|
602
|
-
// Success payloads cross toward the renderer and the transcript too; a
|
|
603
|
-
// server can embed the credential it was just sent into a result.
|
|
604
|
-
const published = {
|
|
605
|
-
content: deepScrub(result.content.map(normalizeContent), inventory),
|
|
606
|
-
structuredContent: deepScrub(result.structuredContent, inventory),
|
|
607
|
-
};
|
|
608
|
-
assertSuccessfulToolResultBudget(serverId, toolName, published);
|
|
609
|
-
return published;
|
|
610
771
|
}
|
|
611
772
|
async test(serverId) {
|
|
612
773
|
const started = this.now();
|
|
@@ -633,6 +794,14 @@ export class McpClientManager {
|
|
|
633
794
|
async connectEntry(serverId, entry, signal) {
|
|
634
795
|
let connected;
|
|
635
796
|
entry.closing = false;
|
|
797
|
+
entry.diagnosticPrivacy = {
|
|
798
|
+
states: new Set(),
|
|
799
|
+
failure: new AbortController(),
|
|
800
|
+
bytes: 0,
|
|
801
|
+
pendingCalls: 0,
|
|
802
|
+
stderrSuppressed: false,
|
|
803
|
+
exhausted: false,
|
|
804
|
+
};
|
|
636
805
|
entry.refreshDiagnostic = undefined;
|
|
637
806
|
entry.subscription = undefined;
|
|
638
807
|
entry.subscriptionDiagnostic = undefined;
|
|
@@ -793,7 +962,7 @@ export class McpClientManager {
|
|
|
793
962
|
env: buildStdioEnvironment(entry.config.env, process.env, this.excludedStdioEnvironmentKeys),
|
|
794
963
|
stderr: 'pipe',
|
|
795
964
|
});
|
|
796
|
-
attachStderrTail(transport, entry,
|
|
965
|
+
attachStderrTail(transport, entry, entry.diagnosticPrivacy, () => this.secretsFor(serverId, entry.config), () => {
|
|
797
966
|
if (this.connections.get(serverId) === entry)
|
|
798
967
|
this.emit(entry.status);
|
|
799
968
|
});
|
|
@@ -1587,6 +1756,13 @@ export class McpClientManager {
|
|
|
1587
1756
|
entry.toolSnapshot = snapshot;
|
|
1588
1757
|
this.bindingIndex = nextIndex;
|
|
1589
1758
|
this.callableSnapshot = nextCallableSnapshot;
|
|
1759
|
+
// Notify after publishing the replacement so every resumed callback sees
|
|
1760
|
+
// the same retired binding. Unchanged refreshes preserve binding tokens.
|
|
1761
|
+
for (const [controller, call] of this.formCalls) {
|
|
1762
|
+
if (call.serverId === entry.status.serverId && !nextIndex.has(call.binding)) {
|
|
1763
|
+
controller.abort(new Error('MCP tool binding is stale'));
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1590
1766
|
}
|
|
1591
1767
|
buildCallableSnapshot(replacedEntry, replacement) {
|
|
1592
1768
|
const entries = this.closed
|
|
@@ -2030,14 +2206,73 @@ export function buildStdioEnvironment(explicit = {}, source = process.env, exclu
|
|
|
2030
2206
|
}
|
|
2031
2207
|
return environment;
|
|
2032
2208
|
}
|
|
2033
|
-
function
|
|
2209
|
+
function exhaustConnectionDiagnosticPrivacy(entry, privacy) {
|
|
2210
|
+
privacy.exhausted = true;
|
|
2211
|
+
privacy.failure.abort(new Error(FORM_PRIVACY_EXHAUSTED));
|
|
2212
|
+
privacy.stderrSuppressed = true;
|
|
2213
|
+
privacy.discardPendingStderr?.();
|
|
2214
|
+
entry.status = { ...entry.status, stderrTail: undefined };
|
|
2215
|
+
}
|
|
2216
|
+
function retainConnectionFormState(entry, privacy, state) {
|
|
2217
|
+
if (/[^\x20-\x7e]/u.test(state)) {
|
|
2218
|
+
// Line splitting/formatting can remove controls, and chunk-wise UTF-8
|
|
2219
|
+
// decoding can split non-ASCII state bytes. Never publish this generation's
|
|
2220
|
+
// stderr when exact raw-string replacement cannot cover those transforms.
|
|
2221
|
+
privacy.stderrSuppressed = true;
|
|
2222
|
+
privacy.discardPendingStderr?.();
|
|
2223
|
+
}
|
|
2224
|
+
if (privacy.states.has(state))
|
|
2225
|
+
return true;
|
|
2226
|
+
const bytes = Buffer.byteLength(state);
|
|
2227
|
+
if (privacy.states.size >= MAX_CONNECTION_FORM_STATES ||
|
|
2228
|
+
privacy.bytes + bytes > MAX_CONNECTION_FORM_STATE_BYTES) {
|
|
2229
|
+
exhaustConnectionDiagnosticPrivacy(entry, privacy);
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
privacy.states.add(state);
|
|
2233
|
+
privacy.bytes += bytes;
|
|
2234
|
+
return true;
|
|
2235
|
+
}
|
|
2236
|
+
function attachStderrTail(transport, entry, privacy, secrets, onUpdate) {
|
|
2034
2237
|
let pending = '';
|
|
2035
2238
|
let oversized = false;
|
|
2036
2239
|
let discardingContinuation = false;
|
|
2037
2240
|
let physicalSuffix = '';
|
|
2241
|
+
let sensitivePartialLine = false;
|
|
2242
|
+
let sensitiveSuffix = '';
|
|
2243
|
+
const discardSensitive = (value, all) => {
|
|
2244
|
+
let offset = 0;
|
|
2245
|
+
while (offset < value.length && (all || sensitivePartialLine)) {
|
|
2246
|
+
const newline = value.indexOf('\n', offset);
|
|
2247
|
+
const part = value.slice(offset, newline < 0 ? undefined : newline);
|
|
2248
|
+
sensitiveSuffix = part.length >= 2 ? part.slice(-2) : `${sensitiveSuffix}${part}`.slice(-2);
|
|
2249
|
+
if (newline < 0) {
|
|
2250
|
+
sensitivePartialLine = true;
|
|
2251
|
+
return value.length;
|
|
2252
|
+
}
|
|
2253
|
+
sensitivePartialLine = sensitiveSuffix.endsWith('\\') || sensitiveSuffix.endsWith('\\\r');
|
|
2254
|
+
sensitiveSuffix = '';
|
|
2255
|
+
offset = newline + 1;
|
|
2256
|
+
}
|
|
2257
|
+
return offset;
|
|
2258
|
+
};
|
|
2259
|
+
const current = () => entry.diagnosticPrivacy === privacy && !entry.closing;
|
|
2260
|
+
const suppressed = () => privacy.stderrSuppressed || privacy.pendingCalls > 0;
|
|
2261
|
+
privacy.discardPendingStderr = () => {
|
|
2262
|
+
if (pending.length > 0 || oversized || discardingContinuation) {
|
|
2263
|
+
sensitivePartialLine = true;
|
|
2264
|
+
sensitiveSuffix = physicalSuffix;
|
|
2265
|
+
}
|
|
2266
|
+
pending = '';
|
|
2267
|
+
oversized = false;
|
|
2268
|
+
discardingContinuation = false;
|
|
2269
|
+
physicalSuffix = '';
|
|
2270
|
+
};
|
|
2038
2271
|
const append = (lines) => {
|
|
2272
|
+
if (!current() || suppressed())
|
|
2273
|
+
return;
|
|
2039
2274
|
const rendered = lines
|
|
2040
|
-
.map((line) => formatMcpDiagnosticText(scrubKnownSecrets(line, secrets), STDERR_LINE_CHARS))
|
|
2275
|
+
.map((line) => formatMcpDiagnosticText(scrubKnownSecrets(line, secrets()), STDERR_LINE_CHARS))
|
|
2041
2276
|
.filter(Boolean);
|
|
2042
2277
|
if (rendered.length === 0)
|
|
2043
2278
|
return;
|
|
@@ -2094,8 +2329,15 @@ function attachStderrTail(transport, entry, secrets, onUpdate) {
|
|
|
2094
2329
|
const stream = transport.stderr;
|
|
2095
2330
|
stream?.on('data', (chunk) => {
|
|
2096
2331
|
const batch = [];
|
|
2332
|
+
if (!current())
|
|
2333
|
+
return;
|
|
2097
2334
|
const value = String(chunk);
|
|
2098
|
-
|
|
2335
|
+
if (suppressed()) {
|
|
2336
|
+
privacy.discardPendingStderr?.();
|
|
2337
|
+
discardSensitive(value, true);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
let offset = discardSensitive(value, false);
|
|
2099
2341
|
for (let newline = value.indexOf('\n', offset); newline >= 0;) {
|
|
2100
2342
|
consume(value.slice(offset, newline), true, batch);
|
|
2101
2343
|
offset = newline + 1;
|
|
@@ -2105,6 +2347,8 @@ function attachStderrTail(transport, entry, secrets, onUpdate) {
|
|
|
2105
2347
|
append(batch);
|
|
2106
2348
|
});
|
|
2107
2349
|
const flush = () => {
|
|
2350
|
+
if (!current() || suppressed() || sensitivePartialLine)
|
|
2351
|
+
return;
|
|
2108
2352
|
if (!pending && !oversized && !discardingContinuation)
|
|
2109
2353
|
return;
|
|
2110
2354
|
const batch = [];
|
|
@@ -2467,3 +2711,25 @@ class McpAbandonSupersededError extends Error {
|
|
|
2467
2711
|
function isRecord(value) {
|
|
2468
2712
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2469
2713
|
}
|
|
2714
|
+
/** Race even handlers that do not accept a cancellation signal, and always
|
|
2715
|
+
* consume a late callback rejection after the invocation has been released. */
|
|
2716
|
+
async function waitForMcpForm(request, signal) {
|
|
2717
|
+
signal.throwIfAborted();
|
|
2718
|
+
let abort;
|
|
2719
|
+
const cancelled = new Promise((_, reject) => {
|
|
2720
|
+
abort = () => reject(signal.reason ?? new Error('MCP form cancelled'));
|
|
2721
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
2722
|
+
});
|
|
2723
|
+
try {
|
|
2724
|
+
return await Promise.race([
|
|
2725
|
+
Promise.resolve().then(() => {
|
|
2726
|
+
signal.throwIfAborted();
|
|
2727
|
+
return request();
|
|
2728
|
+
}),
|
|
2729
|
+
cancelled,
|
|
2730
|
+
]);
|
|
2731
|
+
}
|
|
2732
|
+
finally {
|
|
2733
|
+
signal.removeEventListener('abort', abort);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
@@ -61,19 +61,6 @@ export async function inspectAgentRunReadModel(runStore, runtimeEventStore, opti
|
|
|
61
61
|
diagnostics,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
export async function inspectSessionRunReadModels(runStore, runtimeEventStore, sessionId, options = {}) {
|
|
65
|
-
const invocations = await runtimeEventStore.listSessionInvocations(sessionId);
|
|
66
|
-
const models = [];
|
|
67
|
-
for (const invocation of invocations) {
|
|
68
|
-
models.push(await inspectAgentRunReadModel(runStore, runtimeEventStore, {
|
|
69
|
-
sessionId,
|
|
70
|
-
runId: invocation.runId,
|
|
71
|
-
invocation,
|
|
72
|
-
...(options.isFatalReadError ? { isFatalReadError: options.isFatalReadError } : {}),
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
|
-
return models;
|
|
76
|
-
}
|
|
77
64
|
// A run is not its invocation: a continuation is a new run on the invocation it
|
|
78
65
|
// resumes. This reader is addressed by run, so it looks the invocation up by the
|
|
79
66
|
// id it was actually given.
|
|
@@ -20,12 +20,13 @@ import { RUN_COMPOSITION_RECORDED_EVENT_TYPE } from '@maka/core/agent-run';
|
|
|
20
20
|
import { isRuntimeHandoffPause } from '@maka/core/runtime-handoff';
|
|
21
21
|
import { RunHandoffGate } from './run-handoff-gate.js';
|
|
22
22
|
import { preserveHandoffOpening } from './runtime-resume.js';
|
|
23
|
-
import { decodeRunCompositionSnapshot } from '@maka/core/run-composition';
|
|
23
|
+
import { createRequestCompositionSnapshot, decodeRequestCompositionSnapshot, decodeRunCompositionSnapshot, } from '@maka/core/run-composition';
|
|
24
24
|
import { DurableStoreWriteError, RunSealedError } from '@maka/core/runtime-event-store';
|
|
25
25
|
import { buildInvocationOpenedEvent, buildSyntheticTerminalRuntimeEvent, isSessionInlineInvocation, } from '@maka/core/runtime-invocation';
|
|
26
26
|
import { MODEL_PROJECTION_TRANSITION_EVENT_TYPE, } from '@maka/core/model-projection-transition';
|
|
27
27
|
import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
|
|
28
28
|
import { ToolLedgerCorruptionError, ToolLedgerRejectionError, } from '@maka/core/tool-ledger-scanner';
|
|
29
|
+
import { stableHash } from './request-shape.js';
|
|
29
30
|
import { Buffer } from 'node:buffer';
|
|
30
31
|
import { isDeepStrictEqual } from 'node:util';
|
|
31
32
|
import { redactSecrets } from '@maka/core/redaction';
|
|
@@ -79,6 +80,9 @@ export class AgentRun {
|
|
|
79
80
|
runComposition;
|
|
80
81
|
runCompositionWrite;
|
|
81
82
|
runCompositionCommitted = false;
|
|
83
|
+
requestComposition;
|
|
84
|
+
requestCompositionIndex;
|
|
85
|
+
requestCompositionIndexRead;
|
|
82
86
|
failureClass;
|
|
83
87
|
failureMessage;
|
|
84
88
|
lastTs = 0;
|
|
@@ -418,6 +422,72 @@ export class AgentRun {
|
|
|
418
422
|
throw new Error('Cooperative handoff requires a durably committed Run Composition');
|
|
419
423
|
}
|
|
420
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Durably binds one logical model step to its effective request surface.
|
|
427
|
+
* Unchanged steps reuse the latest snapshot; a changed surface appends a full
|
|
428
|
+
* replacement before provider dispatch, matching DSH request/header epochs.
|
|
429
|
+
*/
|
|
430
|
+
async recordRequestComposition(input) {
|
|
431
|
+
if (!this.input.runStore) {
|
|
432
|
+
throw new Error('AgentRun store is not configured');
|
|
433
|
+
}
|
|
434
|
+
await this.loadRequestCompositionIndex();
|
|
435
|
+
const snapshot = createRequestCompositionSnapshot(input, this.requestCompositionIndex?.size ? 'change' : 'initial');
|
|
436
|
+
const surfaceHash = requestCompositionSurfaceHash(snapshot);
|
|
437
|
+
const existing = this.requestCompositionIndex?.get(surfaceHash);
|
|
438
|
+
if (existing) {
|
|
439
|
+
if (!sameRequestCompositionSurface(existing, snapshot)) {
|
|
440
|
+
throw new Error(`Request Composition surface hash collision: ${surfaceHash}`);
|
|
441
|
+
}
|
|
442
|
+
this.requestComposition = existing;
|
|
443
|
+
return existing.compositionId;
|
|
444
|
+
}
|
|
445
|
+
await this.enqueueRequiredRunStoreWrite('append request composition', async () => {
|
|
446
|
+
await this.input.runStore?.appendEvent(this.sessionId, this.runId, {
|
|
447
|
+
type: 'request_composition_resolved',
|
|
448
|
+
id: snapshot.compositionId,
|
|
449
|
+
runId: this.runId,
|
|
450
|
+
sessionId: this.sessionId,
|
|
451
|
+
turnId: this.turnId,
|
|
452
|
+
ts: this.input.now(),
|
|
453
|
+
data: { snapshot },
|
|
454
|
+
}, { durable: true });
|
|
455
|
+
});
|
|
456
|
+
this.requestComposition = snapshot;
|
|
457
|
+
this.requestCompositionIndex?.set(surfaceHash, snapshot);
|
|
458
|
+
return snapshot.compositionId;
|
|
459
|
+
}
|
|
460
|
+
async loadRequestCompositionIndex() {
|
|
461
|
+
if (this.requestCompositionIndex)
|
|
462
|
+
return;
|
|
463
|
+
if (this.requestCompositionIndexRead)
|
|
464
|
+
return await this.requestCompositionIndexRead;
|
|
465
|
+
const read = (async () => {
|
|
466
|
+
const index = new Map();
|
|
467
|
+
const events = await this.input.runStore?.readEvents(this.sessionId, this.runId);
|
|
468
|
+
for (const event of events ?? []) {
|
|
469
|
+
if (event.type !== 'request_composition_resolved')
|
|
470
|
+
continue;
|
|
471
|
+
const snapshot = decodeRequestCompositionSnapshot(event.data?.snapshot);
|
|
472
|
+
const surfaceHash = requestCompositionSurfaceHash(snapshot);
|
|
473
|
+
const existing = index.get(surfaceHash);
|
|
474
|
+
if (existing && !sameRequestCompositionSurface(existing, snapshot)) {
|
|
475
|
+
throw new Error(`Request Composition surface hash collision: ${surfaceHash}`);
|
|
476
|
+
}
|
|
477
|
+
index.set(surfaceHash, existing ?? snapshot);
|
|
478
|
+
this.requestComposition = snapshot;
|
|
479
|
+
}
|
|
480
|
+
this.requestCompositionIndex = index;
|
|
481
|
+
})();
|
|
482
|
+
this.requestCompositionIndexRead = read;
|
|
483
|
+
try {
|
|
484
|
+
await read;
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
if (this.requestCompositionIndexRead === read)
|
|
488
|
+
this.requestCompositionIndexRead = undefined;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
421
491
|
/**
|
|
422
492
|
* Canonical accounting record for one physical provider request (#1679).
|
|
423
493
|
*
|
|
@@ -621,13 +691,22 @@ export class AgentRun {
|
|
|
621
691
|
modelId: this.header.model,
|
|
622
692
|
};
|
|
623
693
|
}
|
|
624
|
-
async
|
|
694
|
+
async beginUserTurn() {
|
|
625
695
|
// Owed from here, not from after the opening: `openInvocation` can leave the
|
|
626
696
|
// invocation open and still throw, and `finalize` reopens what it can.
|
|
627
697
|
this.initialRuntimeEventPending = true;
|
|
628
698
|
await this.openInvocation();
|
|
629
699
|
this.lastTs = this.input.now();
|
|
630
700
|
const initialRuntimeEvent = await this.recordInitialRuntimeEvent(this.lastTs);
|
|
701
|
+
return initialRuntimeEvent;
|
|
702
|
+
}
|
|
703
|
+
/** Host actions share Turn facts and finalization without activating a provider. */
|
|
704
|
+
async beginCoordination() {
|
|
705
|
+
await this.beginUserTurn();
|
|
706
|
+
await this.input.hooks.updateStatus(this.sessionId, 'running', undefined, this.lastTs);
|
|
707
|
+
}
|
|
708
|
+
async begin() {
|
|
709
|
+
const initialRuntimeEvent = await this.beginUserTurn();
|
|
631
710
|
this.active = await this.input.hooks.reserveRun(this.sessionId, this.header, this);
|
|
632
711
|
await this.input.hooks.updateStatus(this.sessionId, 'running', undefined, this.lastTs);
|
|
633
712
|
const priorRuntimeContext = await this.buildPriorRuntimeContext();
|
|
@@ -1573,6 +1652,15 @@ function redactTraceString(value) {
|
|
|
1573
1652
|
function errorMessage(error) {
|
|
1574
1653
|
return redactTraceString(error instanceof Error ? error.message : String(error));
|
|
1575
1654
|
}
|
|
1655
|
+
function sameRequestCompositionSurface(current, candidate) {
|
|
1656
|
+
const { schemaVersion: _schemaVersion, compositionId: _compositionId, step: _step, reason: _reason, ...currentSurface } = current;
|
|
1657
|
+
const { schemaVersion: _candidateSchemaVersion, compositionId: _candidateCompositionId, step: _candidateStep, reason: _candidateReason, ...candidateSurface } = candidate;
|
|
1658
|
+
return isDeepStrictEqual(currentSurface, candidateSurface);
|
|
1659
|
+
}
|
|
1660
|
+
function requestCompositionSurfaceHash(snapshot) {
|
|
1661
|
+
const { schemaVersion: _schemaVersion, compositionId: _compositionId, step: _step, reason: _reason, ...surface } = snapshot;
|
|
1662
|
+
return stableHash(surface);
|
|
1663
|
+
}
|
|
1576
1664
|
function isInteractionResumeAck(event) {
|
|
1577
1665
|
return (event.type === 'sandbox_boundary_decision_ack' ||
|
|
1578
1666
|
event.type === 'user_question_answer_ack' ||
|