@gajae-code/coding-agent 0.16.3 → 0.16.6
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/CHANGELOG.md +113 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +35 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +26 -1
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +112 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +10 -0
- package/dist/types/task/executor.d.ts +5 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +10 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +9 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +11 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli/root-flags.ts +5 -3
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +379 -31
- package/src/config/model-preset-registry.ts +120 -19
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +73 -23
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +52 -55
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +307 -34
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +228 -60
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +149 -190
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +73 -25
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +143 -10
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +12 -12
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +78 -35
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +396 -22
- package/src/modes/controllers/command-controller.ts +3 -0
- package/src/modes/controllers/event-controller.ts +151 -7
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +53 -6
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +17 -3
- package/src/modes/utils/ui-helpers.ts +2 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +189 -14
- package/src/runtime-mcp/pool.test.ts +218 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +124 -32
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +478 -49
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +11 -8
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +52 -10
- package/src/sdk/host/session-runtime.ts +383 -44
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +77 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +228 -55
- package/src/sdk/transport/relay.ts +35 -13
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +240 -90
- package/src/session/contribution-prep.ts +214 -21
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +456 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +29 -3
- package/src/task/executor.ts +7 -1
- package/src/task/index.ts +21 -4
- package/src/tools/bash.ts +63 -19
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +10 -0
- package/src/tools/job.ts +21 -4
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/search.ts +116 -58
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +37 -2
- package/src/tools/subagent.ts +6 -3
- package/src/tools/tool-catalog.generated.ts +10 -3
- package/src/tools/ultragoal-ask-guard.ts +2 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
|
@@ -4,7 +4,7 @@ import * as crypto from "node:crypto";
|
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
7
|
+
import { isContinuingMidRunMaintenanceOutcome, ThinkingLevel } from "@gajae-code/agent-core";
|
|
8
8
|
import type { Api, ImageContent, Model } from "@gajae-code/ai/core";
|
|
9
9
|
import { logger } from "@gajae-code/utils";
|
|
10
10
|
import { AsyncJobManager } from "../../async";
|
|
@@ -27,7 +27,10 @@ import { resolveModelChainWithAuth, splitSelectorThinkingSuffix } from "../../co
|
|
|
27
27
|
import { type ModelSelectorValue, normalizeModelSelectorValue } from "../../config/model-selector-value";
|
|
28
28
|
import { type Settings, validateSettingPatch } from "../../config/settings";
|
|
29
29
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
30
|
+
import type { AgentEndEvent } from "../../extensibility/shared-events";
|
|
30
31
|
import { normalizeGoal } from "../../goals/state";
|
|
32
|
+
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
33
|
+
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
31
34
|
import type { SdkRunCapability } from "../../session/sdk-run-capability";
|
|
32
35
|
import {
|
|
33
36
|
boundTerminalRetentionState,
|
|
@@ -192,6 +195,24 @@ class DiffQueryError extends Error {
|
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Normalize an agent-loop stop reason into the published terminal outcome.
|
|
200
|
+
*
|
|
201
|
+
* Only reached for a boundary that actually terminalizes an owned invocation,
|
|
202
|
+
* so every result is a `stopped` disposition: a failed run publishes its own
|
|
203
|
+
* correlated `agent_failed` frame with the sanitized cause instead.
|
|
204
|
+
*/
|
|
205
|
+
export function terminalStoppedOutcome(
|
|
206
|
+
stopReason: AgentEndEvent["stopReason"],
|
|
207
|
+
maintenanceOutcome: string | undefined,
|
|
208
|
+
): Extract<SdkPromptTerminalOutcome, { kind: "stopped" }> {
|
|
209
|
+
// An aborted maintenance checkpoint is the one maintenance path that really
|
|
210
|
+
// ends the run, and it ends it the same way an explicit stop does.
|
|
211
|
+
return stopReason === "cancelled" || maintenanceOutcome === "aborted"
|
|
212
|
+
? { kind: "stopped", reason: "cancelled", provenance: "client_cancel" }
|
|
213
|
+
: { kind: "stopped", reason: "end_turn", provenance: "agent" };
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
/** Transport-neutral endpoint contract consumed by the SDK session runtime. */
|
|
196
217
|
export interface SessionSdkTransport {
|
|
197
218
|
readonly sessionId: string;
|
|
@@ -395,6 +416,26 @@ export class SessionSdkSessionRuntime {
|
|
|
395
416
|
this.transport.broadcastFrame?.(event);
|
|
396
417
|
}
|
|
397
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Deliver a frame to specific connections without recording it.
|
|
421
|
+
*
|
|
422
|
+
* Deliberately not `emitEvent`: that assigns a ring position and broadcasts to
|
|
423
|
+
* every authenticated socket. Streamed turn content is high-frequency and
|
|
424
|
+
* unbounded in size, so a count-bounded ring would evict the lifecycle events
|
|
425
|
+
* clients depend on -- including the `agent_end` that settles their prompt --
|
|
426
|
+
* and broadcasting would hand every subscriber a stream only the submitter
|
|
427
|
+
* asked for. Delivery failure is ignored: a disconnected consumer must never
|
|
428
|
+
* disturb the turn producing the content.
|
|
429
|
+
*/
|
|
430
|
+
sendFrameTo(connectionId: string, frame: SdkFrame): void {
|
|
431
|
+
try {
|
|
432
|
+
const result = this.transport.sendFrame(connectionId, frame);
|
|
433
|
+
if (result instanceof Promise) result.catch(() => undefined);
|
|
434
|
+
} catch {
|
|
435
|
+
// A dead connection is reaped by the transport's own close handling.
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
398
439
|
publish(frame: SdkFrame): void {
|
|
399
440
|
this.emitEvent(frame);
|
|
400
441
|
}
|
|
@@ -448,13 +489,13 @@ export class SessionSdkSessionRuntime {
|
|
|
448
489
|
}
|
|
449
490
|
}
|
|
450
491
|
|
|
451
|
-
async stop(): Promise<void> {
|
|
492
|
+
async stop(options: { allowLockContention?: boolean } = {}): Promise<void> {
|
|
452
493
|
this.#connectionDisposer?.();
|
|
453
494
|
this.#capabilitiesDisposer?.();
|
|
454
495
|
this.#malformedDisposer?.();
|
|
455
496
|
let hostError: unknown;
|
|
456
497
|
try {
|
|
457
|
-
await this.host.stop();
|
|
498
|
+
await this.host.stop(options);
|
|
458
499
|
} catch (error) {
|
|
459
500
|
hostError = error;
|
|
460
501
|
} finally {
|
|
@@ -577,6 +618,8 @@ function canonicalTerminalOutcome(
|
|
|
577
618
|
if (candidate.kind === "stopped") {
|
|
578
619
|
if (candidate.reason === "cancelled" && candidate.provenance === "client_cancel")
|
|
579
620
|
return { kind: "stopped", reason: "cancelled", provenance: "client_cancel" };
|
|
621
|
+
if (candidate.reason === "end_turn" && candidate.provenance === "agent")
|
|
622
|
+
return { kind: "stopped", reason: "end_turn", provenance: "agent" };
|
|
580
623
|
return undefined;
|
|
581
624
|
}
|
|
582
625
|
if (candidate.kind === "failed")
|
|
@@ -2239,6 +2282,7 @@ function createControlSurface(
|
|
|
2239
2282
|
canResolveGate: () => boolean = () => true,
|
|
2240
2283
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
|
|
2241
2284
|
onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
|
|
2285
|
+
publishLifecycleFrame?: (frame: SdkFrame) => void,
|
|
2242
2286
|
): ControlSurface {
|
|
2243
2287
|
const normalizePromptImages = (value: unknown): ImageContent[] => {
|
|
2244
2288
|
if (!Array.isArray(value)) return [];
|
|
@@ -2376,6 +2420,24 @@ function createControlSurface(
|
|
|
2376
2420
|
const correlation = newCorrelation();
|
|
2377
2421
|
const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
|
|
2378
2422
|
const sdkRunCapability = createSdkRunCapability(sdkRunToken);
|
|
2423
|
+
const publishTerminal = (outcome: InvocationOutcome): void => {
|
|
2424
|
+
publishLifecycleFrame?.({
|
|
2425
|
+
type: "agent_end",
|
|
2426
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
2427
|
+
...correlation,
|
|
2428
|
+
outcome,
|
|
2429
|
+
});
|
|
2430
|
+
};
|
|
2431
|
+
const publishFailure = (error: unknown): void => {
|
|
2432
|
+
const failure = sanitizePromptFailure(error);
|
|
2433
|
+
publishLifecycleFrame?.({
|
|
2434
|
+
type: "agent_failed",
|
|
2435
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
2436
|
+
...correlation,
|
|
2437
|
+
error: failure,
|
|
2438
|
+
});
|
|
2439
|
+
publishTerminal(canonicalFailedOutcome(failure));
|
|
2440
|
+
};
|
|
2379
2441
|
const preflight = Promise.withResolvers<void>();
|
|
2380
2442
|
let accepted = false;
|
|
2381
2443
|
let settled = false;
|
|
@@ -2484,11 +2546,25 @@ function createControlSurface(
|
|
|
2484
2546
|
retirePendingOwner?.(kind, correlation);
|
|
2485
2547
|
const attemptTerminalization = async (remaining: number): Promise<void> => {
|
|
2486
2548
|
try {
|
|
2549
|
+
const before = reconciliation.lookup(kind, correlation) as {
|
|
2550
|
+
status?: string;
|
|
2551
|
+
};
|
|
2552
|
+
const alreadyTerminal = before.status === "terminal_ok" || before.status === "failed";
|
|
2487
2553
|
await reconciliation.noteTransition(kind, correlation, {
|
|
2488
2554
|
type: "agent_end",
|
|
2489
2555
|
...(typeof result === "string" ? { content: sanitizeTurnResultContent(result) } : {}),
|
|
2490
2556
|
});
|
|
2491
2557
|
onInvocationCompletionReconciledForTests?.(kind, correlation);
|
|
2558
|
+
if (!alreadyTerminal) {
|
|
2559
|
+
const settledRecord = reconciliation.lookup(kind, correlation) as {
|
|
2560
|
+
status?: string;
|
|
2561
|
+
outcome?: unknown;
|
|
2562
|
+
error?: { code?: unknown; message?: unknown };
|
|
2563
|
+
};
|
|
2564
|
+
const outcome = canonicalTerminalOutcome(settledRecord.outcome, settledRecord.error);
|
|
2565
|
+
if (outcome?.kind === "failed") publishFailure(settledRecord.error ?? outcome);
|
|
2566
|
+
else if (outcome) publishTerminal(outcome);
|
|
2567
|
+
}
|
|
2492
2568
|
retirePendingOwner?.(kind, correlation, "always");
|
|
2493
2569
|
} catch (transitionError) {
|
|
2494
2570
|
if (kind === "prompt" || kind === "skill") {
|
|
@@ -2550,6 +2626,19 @@ function createControlSurface(
|
|
|
2550
2626
|
// so their recovery owner is this bounded in-place retry loop: the same
|
|
2551
2627
|
// order, retried, until both writes land or the budget is spent.
|
|
2552
2628
|
const attemptRejectionTerminalization = async (remaining: number): Promise<void> => {
|
|
2629
|
+
// AgentSession can reject cleanup after its durable terminal was published.
|
|
2630
|
+
// That rejection is diagnostic, not another outcome for this correlation.
|
|
2631
|
+
const record = reconciliation.lookup(kind, correlation) as { status?: string };
|
|
2632
|
+
if (record.status === "terminal_ok" || record.status === "failed") {
|
|
2633
|
+
logger.warn("SDK submission cleanup failed after terminal publication", {
|
|
2634
|
+
kind,
|
|
2635
|
+
commandId: correlation.commandId,
|
|
2636
|
+
turnId: correlation.turnId,
|
|
2637
|
+
error: sanitizePromptFailure(error),
|
|
2638
|
+
});
|
|
2639
|
+
retirePendingOwner?.(kind, correlation, "always");
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2553
2642
|
try {
|
|
2554
2643
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
|
|
2555
2644
|
} catch {
|
|
@@ -2585,6 +2674,7 @@ function createControlSurface(
|
|
|
2585
2674
|
}
|
|
2586
2675
|
try {
|
|
2587
2676
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
|
|
2677
|
+
publishFailure(error);
|
|
2588
2678
|
} catch (transitionError) {
|
|
2589
2679
|
if (kind === "prompt" || kind === "skill") {
|
|
2590
2680
|
retirePendingOwner?.(
|
|
@@ -3940,7 +4030,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3940
4030
|
waitForGateResolutionQuiescence: () => Promise<void>;
|
|
3941
4031
|
activeInvocation?: { kind: InvocationKind; correlation: InvocationCorrelation };
|
|
3942
4032
|
drainedInvocations?: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
|
|
3943
|
-
attachedInvocations?: Array<{
|
|
4033
|
+
attachedInvocations?: Array<{
|
|
4034
|
+
kind: InvocationKind;
|
|
4035
|
+
correlation: InvocationCorrelation;
|
|
4036
|
+
connectionId: string | undefined;
|
|
4037
|
+
}>;
|
|
3944
4038
|
openLifecycleBatches: Array<{
|
|
3945
4039
|
epoch: number;
|
|
3946
4040
|
invocations: Array<{
|
|
@@ -3949,7 +4043,27 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3949
4043
|
connectionId: string | undefined;
|
|
3950
4044
|
sdkRunToken: string;
|
|
3951
4045
|
}>;
|
|
3952
|
-
attachedInvocations: Array<{
|
|
4046
|
+
attachedInvocations: Array<{
|
|
4047
|
+
kind: InvocationKind;
|
|
4048
|
+
correlation: InvocationCorrelation;
|
|
4049
|
+
connectionId: string | undefined;
|
|
4050
|
+
}>;
|
|
4051
|
+
/**
|
|
4052
|
+
* Content frames observed before this batch's correlated agent_start
|
|
4053
|
+
* reached the wire. The synchronous session subscription can see a
|
|
4054
|
+
* message_update before emitLifecycle finishes awaiting durable start
|
|
4055
|
+
* persistence; holding them here keeps the producer's start/content
|
|
4056
|
+
* boundary for every SDK consumer. Each entry carries the recipients
|
|
4057
|
+
* that owned the run when the event was produced: an owner attached
|
|
4058
|
+
* later must not receive content from before its attachment.
|
|
4059
|
+
*/
|
|
4060
|
+
startPublished: boolean;
|
|
4061
|
+
/** Set once the hold bound dropped content for this batch; gates the single warning. */
|
|
4062
|
+
heldContentDropped: boolean;
|
|
4063
|
+
heldContent: Array<{
|
|
4064
|
+
event: AgentSessionEvent;
|
|
4065
|
+
recipients: Array<{ correlation: InvocationCorrelation; connectionId: string | undefined }>;
|
|
4066
|
+
}>;
|
|
3953
4067
|
}>;
|
|
3954
4068
|
disposeGate?: () => void;
|
|
3955
4069
|
lifecycleActive: boolean;
|
|
@@ -4133,6 +4247,56 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4133
4247
|
owner.lifecycleTasks.add(task);
|
|
4134
4248
|
return task;
|
|
4135
4249
|
};
|
|
4250
|
+
/**
|
|
4251
|
+
* Publish one content frame per owning invocation, each carrying its own
|
|
4252
|
+
* correlation, so a shared run lets every submitter attribute the content to
|
|
4253
|
+
* its own prompt. Content is best-effort and bypasses the lifecycle replay
|
|
4254
|
+
* ring; the turn producing it is authoritative.
|
|
4255
|
+
*/
|
|
4256
|
+
const publishContentFrames = (
|
|
4257
|
+
current: RuntimeState,
|
|
4258
|
+
event: AgentSessionEvent,
|
|
4259
|
+
invocations: ReadonlyArray<{ correlation: InvocationCorrelation; connectionId: string | undefined }>,
|
|
4260
|
+
): void => {
|
|
4261
|
+
try {
|
|
4262
|
+
const payload = toAgentWireEventPayload(event);
|
|
4263
|
+
for (const invocation of invocations) {
|
|
4264
|
+
if (invocation.connectionId === undefined) continue;
|
|
4265
|
+
current.runtime.sendFrameTo(invocation.connectionId, {
|
|
4266
|
+
type: "event",
|
|
4267
|
+
kind: event.type,
|
|
4268
|
+
payload,
|
|
4269
|
+
...invocation.correlation,
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
} catch {
|
|
4273
|
+
// Streamed content is best-effort; the turn producing it is authoritative.
|
|
4274
|
+
}
|
|
4275
|
+
};
|
|
4276
|
+
/**
|
|
4277
|
+
* Upper bound on content events held for one batch while its correlated
|
|
4278
|
+
* agent_start is still being durably recorded. A start write normally settles
|
|
4279
|
+
* in milliseconds, so this is only reached when persistence is wedged. Past
|
|
4280
|
+
* the bound the OLDEST held content is dropped (with one warning per batch):
|
|
4281
|
+
* content is never published before its start, so a wedged write degrades
|
|
4282
|
+
* to a gap in the stream, not an inverted start/content boundary, and it
|
|
4283
|
+
* can never accumulate an entire response in memory. The turn's terminal
|
|
4284
|
+
* result remains authoritative for anything a consumer did not see.
|
|
4285
|
+
*/
|
|
4286
|
+
const MAX_HELD_CONTENT_EVENTS = 256;
|
|
4287
|
+
/**
|
|
4288
|
+
* Release content that arrived before the batch's correlated agent_start
|
|
4289
|
+
* reached the wire. Called once per batch, after the start frames are
|
|
4290
|
+
* published (or after the start publication path gave up), so SDK consumers
|
|
4291
|
+
* observe start before any content, in producer order.
|
|
4292
|
+
*/
|
|
4293
|
+
const releaseHeldContent = (current: RuntimeState, batch: LifecycleBatch): void => {
|
|
4294
|
+
if (batch.startPublished) return;
|
|
4295
|
+
batch.startPublished = true;
|
|
4296
|
+
const held = batch.heldContent;
|
|
4297
|
+
batch.heldContent = [];
|
|
4298
|
+
for (const { event, recipients } of held) publishContentFrames(current, event, recipients);
|
|
4299
|
+
};
|
|
4136
4300
|
const emitLifecycle = async (
|
|
4137
4301
|
type: "agent_start" | "agent_end" | "agent_failed",
|
|
4138
4302
|
ctx: ExtensionContext,
|
|
@@ -4142,6 +4306,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4142
4306
|
terminalContent?: TurnResultContent,
|
|
4143
4307
|
terminalHasActivity = false,
|
|
4144
4308
|
terminalOutcome?: InvocationOutcome,
|
|
4309
|
+
stopReason?: AgentEndEvent["stopReason"],
|
|
4310
|
+
startToken?: string,
|
|
4311
|
+
startTokens?: string[],
|
|
4145
4312
|
): Promise<void> => {
|
|
4146
4313
|
const current = lifecycleOwner?.state ?? active;
|
|
4147
4314
|
if (!current) return;
|
|
@@ -4216,19 +4383,46 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4216
4383
|
// turn (queued-while-streaming submissions never push), so a mid-prompt
|
|
4217
4384
|
// continuation agent_start with an empty queue leaves the current owner
|
|
4218
4385
|
// untouched (review thread P1).
|
|
4219
|
-
const
|
|
4220
|
-
|
|
4386
|
+
const cohort = new Set(startTokens);
|
|
4387
|
+
if (startToken) cohort.add(startToken);
|
|
4388
|
+
const matchingPending =
|
|
4389
|
+
cohort.size > 0 ? current.pending.filter(entry => cohort.has(entry.sdkRunToken)) : current.pending.slice();
|
|
4390
|
+
const matchingKeys = new Set(matchingPending.map(entry => entry.sdkRunToken));
|
|
4391
|
+
const pendingSnapshot = current.pending.splice(0);
|
|
4392
|
+
const drained = pendingSnapshot
|
|
4393
|
+
.filter(entry => matchingKeys.has(entry.sdkRunToken))
|
|
4221
4394
|
.filter(entry => entry.kind !== "prompt" || !current.deadlineManager.isExpiring(entry.correlation));
|
|
4395
|
+
current.pending.push(...pendingSnapshot.filter(entry => !matchingKeys.has(entry.sdkRunToken)));
|
|
4396
|
+
const existingTokenBatch =
|
|
4397
|
+
typeof startToken === "string" && startToken.length > 0
|
|
4398
|
+
? lifecycleRunOwners.get(startToken)?.batch
|
|
4399
|
+
: undefined;
|
|
4222
4400
|
if (drained.length > 0) {
|
|
4223
|
-
|
|
4401
|
+
const batch: LifecycleBatch = {
|
|
4224
4402
|
epoch: current.lifecycleEpoch,
|
|
4225
4403
|
invocations: drained,
|
|
4226
4404
|
attachedInvocations: [],
|
|
4227
|
-
|
|
4405
|
+
startPublished: false,
|
|
4406
|
+
heldContentDropped: false,
|
|
4407
|
+
heldContent: [],
|
|
4408
|
+
};
|
|
4409
|
+
current.openLifecycleBatches.push(batch);
|
|
4410
|
+
for (const entry of drained) {
|
|
4411
|
+
lifecycleRunOwners.set(entry.sdkRunToken, {
|
|
4412
|
+
state: current,
|
|
4413
|
+
batch,
|
|
4414
|
+
correlationKey: lifecycleCorrelationKey(entry.correlation),
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
4228
4417
|
adoptLifecycleBatch(drained);
|
|
4229
4418
|
if (current.activeInvocation?.kind === "prompt") {
|
|
4230
4419
|
current.deadlineManager.onAccepted(current.activeInvocation.correlation);
|
|
4231
4420
|
}
|
|
4421
|
+
} else if (existingTokenBatch && current.openLifecycleBatches.includes(existingTokenBatch)) {
|
|
4422
|
+
// A continuation within the same SDK-owned run carries the same token
|
|
4423
|
+
// but has no newly pending invocation. Preserve the established owner so
|
|
4424
|
+
// later chunks and the final terminal remain directed to its submitters.
|
|
4425
|
+
adoptLifecycleBatch(existingTokenBatch.invocations);
|
|
4232
4426
|
} else {
|
|
4233
4427
|
// An empty drain is an agent-initiated successor run. Clear the
|
|
4234
4428
|
// predecessor's SDK owner and active invocation so abort ownership and
|
|
@@ -4240,7 +4434,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4240
4434
|
// Pair this agent_end with the oldest unmatched start. A delayed
|
|
4241
4435
|
// aborted-turn end that lands after a successor agent_start must
|
|
4242
4436
|
// terminalize the aborted invocation, never the successor.
|
|
4243
|
-
const ended = current.openLifecycleBatches[0];
|
|
4437
|
+
const ended = failureBatch ?? current.openLifecycleBatches[0];
|
|
4244
4438
|
const baseTransitions = ended
|
|
4245
4439
|
? ended.invocations.map(({ kind, correlation }) => ({ kind, correlation }))
|
|
4246
4440
|
: current.activeInvocation
|
|
@@ -4364,7 +4558,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4364
4558
|
};
|
|
4365
4559
|
void attempt().finally(() => skillTerminalRecoveryControllers.delete(recoveryKey));
|
|
4366
4560
|
};
|
|
4367
|
-
if (type === "agent_end" && maintenanceOutcome
|
|
4561
|
+
if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) {
|
|
4368
4562
|
try {
|
|
4369
4563
|
current.runtime.emitEvent({ type, sessionId: ctx.sessionManager.getSessionId() });
|
|
4370
4564
|
} catch {
|
|
@@ -4485,7 +4679,50 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4485
4679
|
observed = false;
|
|
4486
4680
|
}
|
|
4487
4681
|
}
|
|
4682
|
+
} else if (type === "agent_start" && transitions.length > 0) {
|
|
4683
|
+
for (const invocation of transitions) {
|
|
4684
|
+
try {
|
|
4685
|
+
current.runtime.emitEvent({ type, sessionId, ...invocation.correlation });
|
|
4686
|
+
} catch {
|
|
4687
|
+
observed = false;
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
// The start frames are on the wire (or definitively failed); content
|
|
4691
|
+
// that the synchronous session subscription observed meanwhile must
|
|
4692
|
+
// follow them, never precede them.
|
|
4693
|
+
const started = current.openLifecycleBatches.find(candidate =>
|
|
4694
|
+
candidate.invocations.some(entry =>
|
|
4695
|
+
transitions.some(
|
|
4696
|
+
({ correlation }) =>
|
|
4697
|
+
correlation.commandId === entry.correlation.commandId &&
|
|
4698
|
+
correlation.turnId === entry.correlation.turnId,
|
|
4699
|
+
),
|
|
4700
|
+
),
|
|
4701
|
+
);
|
|
4702
|
+
if (started) releaseHeldContent(current, started);
|
|
4703
|
+
} else if (type === "agent_end" && transitions.length > 0) {
|
|
4704
|
+
// An invocation-owned terminal must carry the identity it was accepted
|
|
4705
|
+
// under and the normalized outcome. A client that submitted this turn
|
|
4706
|
+
// matches the terminal against its own acknowledged correlation and
|
|
4707
|
+
// refuses an unowned one, so the bare `{ type, sessionId }` boundary left
|
|
4708
|
+
// every externally submitted prompt reporting `working` forever even
|
|
4709
|
+
// though the turn had finished -- ACP dropped it as
|
|
4710
|
+
// `incomplete_correlation`, and the notifications runtime has always
|
|
4711
|
+
// stamped both fields for exactly this reason. One frame per invocation,
|
|
4712
|
+
// matching the agent_failed shape above, also lets a shared run attribute
|
|
4713
|
+
// the boundary to each owner instead of one signal nobody can claim. A
|
|
4714
|
+
// continuing maintenance checkpoint never reaches this branch: it returned
|
|
4715
|
+
// above with the one uncorrelated frame, so it cannot settle a live prompt.
|
|
4716
|
+
const outcome = terminalOutcome ?? terminalStoppedOutcome(stopReason, maintenanceOutcome);
|
|
4717
|
+
for (const invocation of transitions) {
|
|
4718
|
+
try {
|
|
4719
|
+
current.runtime.emitEvent({ type, sessionId, ...invocation.correlation, outcome });
|
|
4720
|
+
} catch {
|
|
4721
|
+
observed = false;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4488
4724
|
} else {
|
|
4725
|
+
// Host-originated runs without an invocation keep an uncorrelated frame.
|
|
4489
4726
|
try {
|
|
4490
4727
|
current.runtime.emitEvent({ type, sessionId });
|
|
4491
4728
|
} catch {
|
|
@@ -4495,7 +4732,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4495
4732
|
} catch {
|
|
4496
4733
|
observed = false;
|
|
4497
4734
|
}
|
|
4498
|
-
if (type === "agent_end" && maintenanceOutcome
|
|
4735
|
+
if (type === "agent_end" && isContinuingMidRunMaintenanceOutcome(maintenanceOutcome)) return;
|
|
4499
4736
|
if (type === "agent_end") {
|
|
4500
4737
|
if (current.lifecycleEpoch !== eventLifecycleEpoch) {
|
|
4501
4738
|
// A successor agent_start won the lifecycle race while this event's
|
|
@@ -4553,29 +4790,23 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4553
4790
|
};
|
|
4554
4791
|
api.on("agent_start", (event, ctx) => {
|
|
4555
4792
|
const owner = lifecycleStateForEvent(ctx, "agent_start", event.sdkRunToken);
|
|
4556
|
-
return trackLifecycle(
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
: entry
|
|
4574
|
-
? { correlationKey: lifecycleCorrelationKey(entry.correlation) }
|
|
4575
|
-
: {}),
|
|
4576
|
-
});
|
|
4577
|
-
}
|
|
4578
|
-
}, owner);
|
|
4793
|
+
return trackLifecycle(
|
|
4794
|
+
async () =>
|
|
4795
|
+
emitLifecycle(
|
|
4796
|
+
"agent_start",
|
|
4797
|
+
ctx,
|
|
4798
|
+
undefined,
|
|
4799
|
+
undefined,
|
|
4800
|
+
owner ? { state: owner, sessionId: owner.sessionId } : undefined,
|
|
4801
|
+
undefined,
|
|
4802
|
+
false,
|
|
4803
|
+
undefined,
|
|
4804
|
+
undefined,
|
|
4805
|
+
typeof event.sdkRunToken === "string" ? event.sdkRunToken : undefined,
|
|
4806
|
+
event.sdkRunTokens,
|
|
4807
|
+
),
|
|
4808
|
+
owner,
|
|
4809
|
+
);
|
|
4579
4810
|
});
|
|
4580
4811
|
api.on("agent_end", (event, ctx) => {
|
|
4581
4812
|
const tokenBinding =
|
|
@@ -4608,11 +4839,20 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4608
4839
|
const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
|
|
4609
4840
|
const terminalEvidence = promptTerminalEvidenceFromAgentEnd(event);
|
|
4610
4841
|
const terminalOutcome =
|
|
4611
|
-
event.stopReason === "cancelled"
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4842
|
+
event.stopReason === "cancelled" ||
|
|
4843
|
+
(event.stopReason === "maintenance" && event.maintenanceOutcome === "aborted")
|
|
4844
|
+
? terminalStoppedOutcome(
|
|
4845
|
+
event.stopReason,
|
|
4846
|
+
event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
|
|
4847
|
+
)
|
|
4848
|
+
: failure !== undefined
|
|
4849
|
+
? canonicalFailedOutcome(failure)
|
|
4850
|
+
: terminalEvidence.content?.text.trim() || terminalEvidence.hasActivity
|
|
4851
|
+
? terminalStoppedOutcome(
|
|
4852
|
+
event.stopReason,
|
|
4853
|
+
event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
|
|
4854
|
+
)
|
|
4855
|
+
: canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
|
|
4616
4856
|
return trackLifecycle(async () => {
|
|
4617
4857
|
if (failure && !failureAlreadyPublished) {
|
|
4618
4858
|
for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
|
|
@@ -4627,6 +4867,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4627
4867
|
terminalEvidence.content,
|
|
4628
4868
|
terminalEvidence.hasActivity,
|
|
4629
4869
|
terminalOutcome,
|
|
4870
|
+
event.stopReason,
|
|
4630
4871
|
);
|
|
4631
4872
|
}, owner).finally(() => {
|
|
4632
4873
|
if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
|
|
@@ -4716,6 +4957,74 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4716
4957
|
current.deadlineManager.onAttributableEvent(invocation.correlation, eventType);
|
|
4717
4958
|
}
|
|
4718
4959
|
};
|
|
4960
|
+
/**
|
|
4961
|
+
* Stream one turn event to the connections that submitted the running turn.
|
|
4962
|
+
*
|
|
4963
|
+
* This is the same frame the notifications runtime sends for an ACP-launched
|
|
4964
|
+
* session (`{ type: "event", kind, payload: { event_type, event }, ...correlation }`),
|
|
4965
|
+
* built from the same shared producer, so an ACP client attached to a live
|
|
4966
|
+
* interactive session renders assistant text and tool calls exactly as it does
|
|
4967
|
+
* for a session it started itself. Without it an attached client could drive
|
|
4968
|
+
* the session and see the turn settle but never receive a single word of the
|
|
4969
|
+
* answer.
|
|
4970
|
+
*
|
|
4971
|
+
* Scoped to the owning invocations of the batch that is actually running: a
|
|
4972
|
+
* turn nobody submitted over the SDK (an ordinary terminal prompt, an
|
|
4973
|
+
* autonomous continuation, cron, monitor) has no owner connection and streams
|
|
4974
|
+
* nothing at all, so a session with no attached client pays one map lookup.
|
|
4975
|
+
*/
|
|
4976
|
+
const STREAMED_TURN_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
4977
|
+
"message_update",
|
|
4978
|
+
"message_end",
|
|
4979
|
+
"tool_execution_start",
|
|
4980
|
+
"tool_execution_update",
|
|
4981
|
+
"tool_execution_end",
|
|
4982
|
+
]);
|
|
4983
|
+
const streamTurnEvent = (event: AgentSessionEvent | null | undefined, ctx: ExtensionContext): void => {
|
|
4984
|
+
const current = lifecycleStateForContext(ctx, "agent_start");
|
|
4985
|
+
const activeInvocation = current?.activeInvocation;
|
|
4986
|
+
if (!current) return;
|
|
4987
|
+
const batch = activeInvocation
|
|
4988
|
+
? current.openLifecycleBatches.find(candidate =>
|
|
4989
|
+
candidate.invocations.some(
|
|
4990
|
+
entry =>
|
|
4991
|
+
entry.correlation.commandId === activeInvocation.correlation.commandId &&
|
|
4992
|
+
entry.correlation.turnId === activeInvocation.correlation.turnId,
|
|
4993
|
+
),
|
|
4994
|
+
)
|
|
4995
|
+
: undefined;
|
|
4996
|
+
const invocations = batch
|
|
4997
|
+
? [...batch.invocations, ...batch.attachedInvocations]
|
|
4998
|
+
: current.lifecycleActive
|
|
4999
|
+
? (current.attachedInvocations ?? [])
|
|
5000
|
+
: [];
|
|
5001
|
+
if (invocations.length === 0) return;
|
|
5002
|
+
// Content bypasses the lifecycle replay ring and must never interrupt its producer.
|
|
5003
|
+
if (!event || typeof event.type !== "string" || !STREAMED_TURN_EVENT_TYPES.has(event.type)) return;
|
|
5004
|
+
// emitLifecycle("agent_start") awaits durable persistence before it
|
|
5005
|
+
// publishes the correlated start frame, while this subscription is
|
|
5006
|
+
// synchronous. Hold content until that start is on the wire so no consumer
|
|
5007
|
+
// sees turn content before the turn it belongs to.
|
|
5008
|
+
if (batch && !batch.startPublished) {
|
|
5009
|
+
batch.heldContent.push({ event: event as AgentSessionEvent, recipients: invocations });
|
|
5010
|
+
if (batch.heldContent.length > MAX_HELD_CONTENT_EVENTS) {
|
|
5011
|
+
if (!batch.heldContentDropped) {
|
|
5012
|
+
batch.heldContentDropped = true;
|
|
5013
|
+
logger.warn(
|
|
5014
|
+
"sdk: agent_start persistence still pending; dropping oldest held turn content to bound memory",
|
|
5015
|
+
{
|
|
5016
|
+
epoch: batch.epoch,
|
|
5017
|
+
bound: MAX_HELD_CONTENT_EVENTS,
|
|
5018
|
+
invocations: batch.invocations.length,
|
|
5019
|
+
},
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
5022
|
+
batch.heldContent.splice(0, batch.heldContent.length - MAX_HELD_CONTENT_EVENTS);
|
|
5023
|
+
}
|
|
5024
|
+
return;
|
|
5025
|
+
}
|
|
5026
|
+
publishContentFrames(current, event as AgentSessionEvent, invocations);
|
|
5027
|
+
};
|
|
4719
5028
|
api.on("tool_execution_start", async (_event, ctx) => {
|
|
4720
5029
|
renewAttributableProgress("tool_execution_start", ctx);
|
|
4721
5030
|
});
|
|
@@ -4784,7 +5093,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4784
5093
|
connectionId: string | undefined;
|
|
4785
5094
|
sdkRunToken: string;
|
|
4786
5095
|
}>;
|
|
4787
|
-
attachedInvocations: Array<{
|
|
5096
|
+
attachedInvocations: Array<{
|
|
5097
|
+
kind: InvocationKind;
|
|
5098
|
+
correlation: InvocationCorrelation;
|
|
5099
|
+
connectionId: string | undefined;
|
|
5100
|
+
}>;
|
|
5101
|
+
startPublished: boolean;
|
|
5102
|
+
heldContentDropped: boolean;
|
|
5103
|
+
heldContent: Array<{
|
|
5104
|
+
event: AgentSessionEvent;
|
|
5105
|
+
recipients: Array<{ correlation: InvocationCorrelation; connectionId: string | undefined }>;
|
|
5106
|
+
}>;
|
|
4788
5107
|
}> = [];
|
|
4789
5108
|
const configRevision = { current: 0 };
|
|
4790
5109
|
let acceptingGateResolutions = true;
|
|
@@ -4913,6 +5232,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4913
5232
|
// 2. Terminal boundary. The durable error from step 1 makes this
|
|
4914
5233
|
// classify as failed, never terminal_ok.
|
|
4915
5234
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
|
|
5235
|
+
const failure = sanitizePromptFailure(Object.assign(new Error(error.message), { code: error.code }));
|
|
5236
|
+
runtime.emitEvent({
|
|
5237
|
+
type: "agent_failed",
|
|
5238
|
+
sessionId,
|
|
5239
|
+
...correlation,
|
|
5240
|
+
error: failure,
|
|
5241
|
+
});
|
|
5242
|
+
runtime.emitEvent({
|
|
5243
|
+
type: "agent_end",
|
|
5244
|
+
sessionId,
|
|
5245
|
+
...correlation,
|
|
5246
|
+
outcome: canonicalFailedOutcome(failure),
|
|
5247
|
+
});
|
|
4916
5248
|
// 3. Release recovery ownership ONLY after durable terminalization.
|
|
4917
5249
|
deadlineManager.clear(correlation);
|
|
4918
5250
|
} catch (transitionError) {
|
|
@@ -5065,7 +5397,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5065
5397
|
entry.correlation.turnId === correlation.turnId,
|
|
5066
5398
|
)
|
|
5067
5399
|
)
|
|
5068
|
-
current.attachedInvocations.push({ kind, correlation });
|
|
5400
|
+
current.attachedInvocations.push({ kind, correlation, connectionId });
|
|
5069
5401
|
if (
|
|
5070
5402
|
activeBatch &&
|
|
5071
5403
|
!activeBatch.attachedInvocations.some(
|
|
@@ -5074,7 +5406,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5074
5406
|
entry.correlation.turnId === correlation.turnId,
|
|
5075
5407
|
)
|
|
5076
5408
|
)
|
|
5077
|
-
activeBatch.attachedInvocations.push({ kind, correlation });
|
|
5409
|
+
activeBatch.attachedInvocations.push({ kind, correlation, connectionId });
|
|
5078
5410
|
bindPromotedToken(activeBatch);
|
|
5079
5411
|
return;
|
|
5080
5412
|
}
|
|
@@ -5157,6 +5489,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5157
5489
|
() => acceptingGateResolutions,
|
|
5158
5490
|
trackGateResolution,
|
|
5159
5491
|
options.onInvocationCompletionReconciledForTests,
|
|
5492
|
+
frame => runtime.emitEvent(frame),
|
|
5160
5493
|
);
|
|
5161
5494
|
const installProviderDefinitions = (capability: string, definitions: unknown): void => {
|
|
5162
5495
|
if (capability === "permission") {
|
|
@@ -5366,9 +5699,15 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5366
5699
|
});
|
|
5367
5700
|
}
|
|
5368
5701
|
});
|
|
5702
|
+
// Extension message_update hooks are queued while final/tool hooks are not.
|
|
5703
|
+
// Use only the synchronous session boundary so those paths cannot reorder or duplicate content.
|
|
5704
|
+
const disposeStream = ctx.onSessionEvent?.(event => {
|
|
5705
|
+
if (STREAMED_TURN_EVENT_TYPES.has(event.type)) streamTurnEvent(event, ctx);
|
|
5706
|
+
});
|
|
5369
5707
|
const disposeGate = (): void => {
|
|
5370
5708
|
disposeWorkflowGate?.();
|
|
5371
5709
|
disposeFold?.();
|
|
5710
|
+
disposeStream?.();
|
|
5372
5711
|
};
|
|
5373
5712
|
let publishedEndpointUrl: string | undefined;
|
|
5374
5713
|
let brokerRegistered = false;
|
|
@@ -5579,7 +5918,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
5579
5918
|
retiredLifecycleOwnerTimers.set(current, timer);
|
|
5580
5919
|
} else current.deadlineManager.clearAll();
|
|
5581
5920
|
current.disposeGate?.();
|
|
5582
|
-
await current.runtime.stop();
|
|
5921
|
+
await current.runtime.stop({ allowLockContention: cancelSkillRecovery });
|
|
5583
5922
|
} catch (error) {
|
|
5584
5923
|
// Keep the immutable owner available for a retry when transport teardown
|
|
5585
5924
|
// fails after quiescing. Clearing `active` before stop prevents a second
|
|
@@ -200,6 +200,7 @@ export interface SessionLifecycleSessionResult {
|
|
|
200
200
|
readonly sessionId: string;
|
|
201
201
|
readonly cwd?: string;
|
|
202
202
|
readonly endpointGeneration?: number;
|
|
203
|
+
readonly endpointIncarnation?: string;
|
|
203
204
|
readonly reused?: boolean;
|
|
204
205
|
readonly note?: string;
|
|
205
206
|
}
|
|
@@ -511,6 +512,7 @@ function sessionResult(value: unknown, expectedSessionId?: string): SessionLifec
|
|
|
511
512
|
sessionId: string;
|
|
512
513
|
cwd?: string;
|
|
513
514
|
endpointGeneration?: number;
|
|
515
|
+
endpointIncarnation?: string;
|
|
514
516
|
reused?: boolean;
|
|
515
517
|
note?: string;
|
|
516
518
|
} = { sessionId };
|
|
@@ -518,6 +520,8 @@ function sessionResult(value: unknown, expectedSessionId?: string): SessionLifec
|
|
|
518
520
|
const endpointGeneration = record.endpointGeneration;
|
|
519
521
|
if (typeof endpointGeneration === "number" && Number.isSafeInteger(endpointGeneration) && endpointGeneration > 0)
|
|
520
522
|
result.endpointGeneration = endpointGeneration;
|
|
523
|
+
if (typeof record.endpointIncarnation === "string" && /^[a-f0-9]{64}$/.test(record.endpointIncarnation))
|
|
524
|
+
result.endpointIncarnation = record.endpointIncarnation;
|
|
521
525
|
if (typeof record.reused === "boolean") result.reused = record.reused;
|
|
522
526
|
if (typeof record.note === "string") result.note = record.note;
|
|
523
527
|
return result;
|