@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0
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 +87 -1
- package/README.md +1 -1
- package/dist/cli.js +5495 -5021
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +350 -50
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/live/controller.d.ts +40 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +48 -0
- package/dist/types/live/visualizer.d.ts +33 -0
- package/dist/types/lsp/client.d.ts +18 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/mcp/tool-bridge.d.ts +7 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +8 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +314 -0
- package/dist/types/session/agent-session.d.ts +120 -750
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +49 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +22 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +143 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +14 -0
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +95 -1
- package/package.json +13 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/usage-cli.ts +11 -2
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +50 -2
- package/src/config/model-resolver.ts +0 -1
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +250 -54
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +89 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/audio-worklet.txt +59 -0
- package/src/live/browser-runtime.txt +221 -0
- package/src/live/controller.ts +464 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +490 -0
- package/src/live/visualizer.ts +234 -0
- package/src/lsp/client.ts +52 -9
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +44 -11
- package/src/main.ts +9 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/render.ts +94 -35
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/mcp/tool-bridge.ts +107 -11
- package/src/mcp/types.ts +7 -0
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +27 -10
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +28 -3
- package/src/modes/components/settings-selector.ts +178 -6
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +70 -22
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/live-command-controller.ts +178 -0
- package/src/modes/controllers/mcp-command-controller.ts +80 -41
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +18 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +160 -38
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/task.md +12 -2
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +255 -139
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +334 -0
- package/src/session/agent-session.ts +1910 -12213
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +212 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +263 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +47 -5
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +10 -51
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +943 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +92 -11
- package/src/slash-commands/helpers/usage-report.ts +25 -4
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +288 -70
- package/src/task/index.ts +148 -111
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +4 -1
- package/src/task/types.ts +16 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +26 -0
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +7 -1
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +67 -3
- package/src/tts/streaming-player.ts +62 -4
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +8 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
3
4
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
4
|
-
import type
|
|
5
|
+
import { type CompactionSummaryMessage, INTERRUPTED_THINKING_MESSAGE_TYPE } from "./messages";
|
|
5
6
|
import { buildSessionContext, type StrippedToolCallsMarker } from "./session-context";
|
|
6
7
|
import type { SessionEntry } from "./session-entries";
|
|
7
8
|
|
|
@@ -159,3 +160,225 @@ describe("buildSessionContext dangling toolCalls", () => {
|
|
|
159
160
|
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
160
161
|
});
|
|
161
162
|
});
|
|
163
|
+
|
|
164
|
+
const assistantUsage: AssistantMessage["usage"] = {
|
|
165
|
+
input: 0,
|
|
166
|
+
output: 0,
|
|
167
|
+
cacheRead: 0,
|
|
168
|
+
cacheWrite: 0,
|
|
169
|
+
totalTokens: 0,
|
|
170
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function userEntry(id: string, parentId: string | null, content: string, messageTimestamp: number): SessionEntry {
|
|
174
|
+
return {
|
|
175
|
+
type: "message",
|
|
176
|
+
id,
|
|
177
|
+
parentId,
|
|
178
|
+
timestamp,
|
|
179
|
+
message: { role: "user", content, timestamp: messageTimestamp } as AgentMessage,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function assistantEntry(
|
|
184
|
+
id: string,
|
|
185
|
+
parentId: string | null,
|
|
186
|
+
stopReason: AssistantMessage["stopReason"],
|
|
187
|
+
text: string,
|
|
188
|
+
messageTimestamp: number,
|
|
189
|
+
): SessionEntry {
|
|
190
|
+
return {
|
|
191
|
+
type: "message",
|
|
192
|
+
id,
|
|
193
|
+
parentId,
|
|
194
|
+
timestamp,
|
|
195
|
+
message: {
|
|
196
|
+
role: "assistant",
|
|
197
|
+
content: [{ type: "text", text }],
|
|
198
|
+
api: "anthropic-messages",
|
|
199
|
+
provider: "anthropic",
|
|
200
|
+
model: "claude-sonnet-4-5",
|
|
201
|
+
usage: assistantUsage,
|
|
202
|
+
stopReason,
|
|
203
|
+
timestamp: messageTimestamp,
|
|
204
|
+
} satisfies AssistantMessage,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function toolCallAssistantEntry(
|
|
209
|
+
id: string,
|
|
210
|
+
parentId: string | null,
|
|
211
|
+
stopReason: AssistantMessage["stopReason"],
|
|
212
|
+
toolCallId: string,
|
|
213
|
+
messageTimestamp: number,
|
|
214
|
+
): SessionEntry {
|
|
215
|
+
return {
|
|
216
|
+
type: "message",
|
|
217
|
+
id,
|
|
218
|
+
parentId,
|
|
219
|
+
timestamp,
|
|
220
|
+
message: {
|
|
221
|
+
role: "assistant",
|
|
222
|
+
content: [{ type: "toolCall", id: toolCallId, name: "write", arguments: { path: "plan.md", content: "x" } }],
|
|
223
|
+
api: "anthropic-messages",
|
|
224
|
+
provider: "anthropic",
|
|
225
|
+
model: "claude-sonnet-4-5",
|
|
226
|
+
usage: assistantUsage,
|
|
227
|
+
stopReason,
|
|
228
|
+
timestamp: messageTimestamp,
|
|
229
|
+
} satisfies AssistantMessage,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function syntheticToolResultEntry(
|
|
234
|
+
id: string,
|
|
235
|
+
parentId: string | null,
|
|
236
|
+
toolCallId: string,
|
|
237
|
+
messageTimestamp: number,
|
|
238
|
+
): SessionEntry {
|
|
239
|
+
return {
|
|
240
|
+
type: "message",
|
|
241
|
+
id,
|
|
242
|
+
parentId,
|
|
243
|
+
timestamp,
|
|
244
|
+
message: {
|
|
245
|
+
role: "toolResult",
|
|
246
|
+
toolCallId,
|
|
247
|
+
toolName: "write",
|
|
248
|
+
content: [
|
|
249
|
+
{ type: "text", text: "Tool call was not executed because the provider stream ended with an error." },
|
|
250
|
+
],
|
|
251
|
+
details: { __synthetic: true, source: "assistant_stop_error", executed: false },
|
|
252
|
+
isError: true,
|
|
253
|
+
timestamp: messageTimestamp,
|
|
254
|
+
} as AgentMessage,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hiddenContinuityEntry(id: string, parentId: string | null): SessionEntry {
|
|
259
|
+
return {
|
|
260
|
+
type: "custom_message",
|
|
261
|
+
id,
|
|
262
|
+
parentId,
|
|
263
|
+
timestamp,
|
|
264
|
+
customType: INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
265
|
+
content: "preserved interrupted thinking",
|
|
266
|
+
display: false,
|
|
267
|
+
attribution: "agent",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function expectUserTail(messages: AgentMessage[], content: string): void {
|
|
272
|
+
const tail = messages.at(-1);
|
|
273
|
+
expect(tail?.role).toBe("user");
|
|
274
|
+
if (tail?.role !== "user") {
|
|
275
|
+
throw new Error(`Expected user tail, received ${tail?.role ?? "none"}`);
|
|
276
|
+
}
|
|
277
|
+
expect(tail.content).toBe(content);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
describe("buildSessionContext failed replay tails", () => {
|
|
281
|
+
it("terminates on cyclic parent links and includes each reachable message once", () => {
|
|
282
|
+
const entries = [userEntry("A", "B", "from A", 1), userEntry("B", "A", "from B", 2)];
|
|
283
|
+
|
|
284
|
+
const context = buildSessionContext(entries, "A");
|
|
285
|
+
|
|
286
|
+
expect(context.messages.map(message => (message.role === "user" ? message.content : message.role))).toEqual([
|
|
287
|
+
"from B",
|
|
288
|
+
"from A",
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("omits a terminal aborted assistant from normal context", () => {
|
|
293
|
+
const context = buildSessionContext([
|
|
294
|
+
userEntry("user", null, "continue", 1),
|
|
295
|
+
assistantEntry("assistant", "user", "aborted", "partial unsafe replay", 2),
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
299
|
+
expectUserTail(context.messages, "continue");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("omits an earlier aborted assistant before a later user from normal context", () => {
|
|
303
|
+
const context = buildSessionContext([
|
|
304
|
+
userEntry("user-1", null, "first prompt", 1),
|
|
305
|
+
assistantEntry("assistant", "user-1", "aborted", "partial unsafe replay", 2),
|
|
306
|
+
userEntry("user-2", "assistant", "retry", 3),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
310
|
+
expectUserTail(context.messages, "retry");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("preserves a terminal aborted assistant in transcript mode", () => {
|
|
314
|
+
const context = buildSessionContext(
|
|
315
|
+
[
|
|
316
|
+
userEntry("user", null, "continue", 1),
|
|
317
|
+
assistantEntry("assistant", "user", "aborted", "visible transcript error", 2),
|
|
318
|
+
],
|
|
319
|
+
undefined,
|
|
320
|
+
undefined,
|
|
321
|
+
{ transcript: true },
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const assistant = context.messages.find(message => message.role === "assistant");
|
|
325
|
+
expect(assistant?.role).toBe("assistant");
|
|
326
|
+
if (assistant?.role !== "assistant") {
|
|
327
|
+
throw new Error(`Expected transcript assistant, received ${assistant?.role ?? "none"}`);
|
|
328
|
+
}
|
|
329
|
+
expect(assistant.stopReason).toBe("aborted");
|
|
330
|
+
expect(assistant.content).toEqual([{ type: "text", text: "visible transcript error" }]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("omits a terminal error assistant from normal context", () => {
|
|
334
|
+
const context = buildSessionContext([
|
|
335
|
+
userEntry("user", null, "retry with smaller input", 1),
|
|
336
|
+
assistantEntry("assistant", "user", "error", "provider rejected the request", 2),
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
340
|
+
expectUserTail(context.messages, "retry with smaller input");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("keeps an aborted assistant when hidden interrupted-thinking continuity follows it", () => {
|
|
344
|
+
const context = buildSessionContext([
|
|
345
|
+
userEntry("user", null, "keep reasoning continuity", 1),
|
|
346
|
+
assistantEntry("assistant", "user", "aborted", "partial answer before interrupt", 2),
|
|
347
|
+
hiddenContinuityEntry("continuity", "assistant"),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const assistant = context.messages.find(message => message.role === "assistant");
|
|
351
|
+
expect(assistant?.role).toBe("assistant");
|
|
352
|
+
if (assistant?.role !== "assistant") {
|
|
353
|
+
throw new Error(`Expected assistant before continuity, received ${assistant?.role ?? "none"}`);
|
|
354
|
+
}
|
|
355
|
+
expect(assistant.stopReason).toBe("aborted");
|
|
356
|
+
expect(context.messages.at(-1)?.role).toBe("custom");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("drops synthetic tool results paired with a dropped failed tool-call turn", () => {
|
|
360
|
+
const context = buildSessionContext([
|
|
361
|
+
userEntry("user", null, "write the plan", 1),
|
|
362
|
+
toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
|
|
363
|
+
syntheticToolResultEntry("result", "assistant", "call-1", 3),
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
expect(context.messages.map(message => message.role)).toEqual(["user"]);
|
|
367
|
+
expectUserTail(context.messages, "write the plan");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("keeps the failed tool-call turn and its result in transcript mode", () => {
|
|
371
|
+
const context = buildSessionContext(
|
|
372
|
+
[
|
|
373
|
+
userEntry("user", null, "write the plan", 1),
|
|
374
|
+
toolCallAssistantEntry("assistant", "user", "error", "call-1", 2),
|
|
375
|
+
syntheticToolResultEntry("result", "assistant", "call-1", 3),
|
|
376
|
+
],
|
|
377
|
+
undefined,
|
|
378
|
+
undefined,
|
|
379
|
+
{ transcript: true, keepDanglingToolCalls: true },
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(context.messages.map(message => message.role)).toEqual(["user", "assistant", "toolResult"]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createBranchSummaryMessage,
|
|
6
6
|
createCompactionSummaryMessage,
|
|
7
7
|
createCustomMessage,
|
|
8
|
+
INTERRUPTED_THINKING_MESSAGE_TYPE,
|
|
8
9
|
isCustomMessageContent,
|
|
9
10
|
normalizeCustomMessagePayload,
|
|
10
11
|
} from "./messages";
|
|
@@ -17,6 +18,8 @@ import { type CompactionEntry, EPHEMERAL_MODEL_CHANGE_ROLE, type SessionEntry }
|
|
|
17
18
|
const LEGACY_SNAPCOMPACT_FRAME_COUNT_GUARD = 16;
|
|
18
19
|
const LEGACY_SNAPCOMPACT_ARCHIVE_TEXT_GUARD = 250_000;
|
|
19
20
|
const LEGACY_SNAPCOMPACT_TRUNCATED_CHARS_GUARD = 1_000_000;
|
|
21
|
+
const SUPERSEDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided after a newer compaction]";
|
|
22
|
+
const SUPERSEDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
|
|
20
23
|
|
|
21
24
|
function hasLegacySnapcompactFrames(archive: snapcompact.Archive): boolean {
|
|
22
25
|
return archive.frames.some(frame => frame.font === undefined && frame.variant === undefined);
|
|
@@ -197,10 +200,13 @@ export function buildSessionContext(
|
|
|
197
200
|
};
|
|
198
201
|
}
|
|
199
202
|
|
|
200
|
-
// Walk from leaf to root, collecting path
|
|
203
|
+
// Walk from leaf to root, collecting path. Corrupt/pre-fix files can contain
|
|
204
|
+
// parent cycles; stop at the first repeat so session load is bounded.
|
|
201
205
|
const path: SessionEntry[] = [];
|
|
206
|
+
const seenPathIds = new Set<string>();
|
|
202
207
|
let current: SessionEntry | undefined = leaf;
|
|
203
|
-
while (current) {
|
|
208
|
+
while (current && !seenPathIds.has(current.id)) {
|
|
209
|
+
seenPathIds.add(current.id);
|
|
204
210
|
path.push(current);
|
|
205
211
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
206
212
|
}
|
|
@@ -340,13 +346,14 @@ export function buildSessionContext(
|
|
|
340
346
|
for (const entry of path) {
|
|
341
347
|
handleEntryResetTracking(entry);
|
|
342
348
|
if (entry.type === "compaction") {
|
|
343
|
-
const
|
|
349
|
+
const active = entry.id === compaction?.id;
|
|
350
|
+
const snapcompactArchive = active ? snapcompact.getPreservedArchive(entry.preserveData) : undefined;
|
|
344
351
|
pushMessage(
|
|
345
352
|
createCompactionSummaryMessage(
|
|
346
|
-
entry.summary,
|
|
353
|
+
active ? entry.summary : SUPERSEDED_COMPACTION_SUMMARY,
|
|
347
354
|
entry.tokensBefore,
|
|
348
355
|
entry.timestamp,
|
|
349
|
-
entry.shortSummary,
|
|
356
|
+
active ? entry.shortSummary : SUPERSEDED_COMPACTION_SHORT_SUMMARY,
|
|
350
357
|
undefined,
|
|
351
358
|
undefined,
|
|
352
359
|
snapcompactHistoryBlocksForContext(snapcompactArchive, options),
|
|
@@ -498,6 +505,41 @@ export function buildSessionContext(
|
|
|
498
505
|
}
|
|
499
506
|
}
|
|
500
507
|
|
|
508
|
+
// Error/abort assistant turns are transcript events, not safe assistant
|
|
509
|
+
// turns to replay into the next provider request. Drop them even when a
|
|
510
|
+
// later user message follows through non-context entries (`session_exit`,
|
|
511
|
+
// labels, etc.); otherwise a resumed session replays a dead partial turn
|
|
512
|
+
// and can spend minutes reprocessing old context before the new prompt.
|
|
513
|
+
// Keep the interrupted-thinking continuity pair: convertToLlm strips the
|
|
514
|
+
// unsafe trailing thinking from that assistant and sends the hidden
|
|
515
|
+
// continuity note instead.
|
|
516
|
+
if (!options?.transcript) {
|
|
517
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
518
|
+
const message = messages[i];
|
|
519
|
+
if (message?.role !== "assistant") continue;
|
|
520
|
+
if (message.stopReason !== "aborted" && message.stopReason !== "error") continue;
|
|
521
|
+
const next = messages[i + 1];
|
|
522
|
+
if (next?.role === "custom" && next.customType === INTERRUPTED_THINKING_MESSAGE_TYPE) continue;
|
|
523
|
+
// A failed turn that emitted tool calls persists paired synthetic
|
|
524
|
+
// tool_result placeholders after it. Dropping only the assistant would
|
|
525
|
+
// strand those results with no preceding tool_use — a shape providers
|
|
526
|
+
// reject — so remove the paired results alongside the turn.
|
|
527
|
+
const droppedToolCallIds = new Set<string>();
|
|
528
|
+
for (const block of message.content) {
|
|
529
|
+
if (block.type === "toolCall") droppedToolCallIds.add(block.id);
|
|
530
|
+
}
|
|
531
|
+
messages.splice(i, 1);
|
|
532
|
+
if (droppedToolCallIds.size > 0) {
|
|
533
|
+
for (let j = messages.length - 1; j >= i; j--) {
|
|
534
|
+
const candidate = messages[j];
|
|
535
|
+
if (candidate?.role === "toolResult" && droppedToolCallIds.has(candidate.toolCallId)) {
|
|
536
|
+
messages.splice(j, 1);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
501
543
|
return {
|
|
502
544
|
messages,
|
|
503
545
|
cacheMissExplainedAt: options?.transcript ? cacheMissExplainedAt : undefined,
|
|
@@ -32,6 +32,12 @@ export interface SessionHeader {
|
|
|
32
32
|
titleSource?: SessionTitleSource;
|
|
33
33
|
timestamp: string;
|
|
34
34
|
cwd: string;
|
|
35
|
+
/**
|
|
36
|
+
* Additional workspace directories beyond `cwd` (multi-root workspace).
|
|
37
|
+
* Absolute, normalized, deduplicated. Absent on legacy single-cwd sessions.
|
|
38
|
+
* See {@link SessionWorkspace} in `./session-workspace`.
|
|
39
|
+
*/
|
|
40
|
+
additionalDirectories?: string[];
|
|
35
41
|
parentSession?: string;
|
|
36
42
|
/** Provider prompt-cache identity inherited by exact-route full forks. */
|
|
37
43
|
providerPromptCacheKey?: string;
|
|
@@ -43,6 +49,8 @@ export interface NewSessionOptions {
|
|
|
43
49
|
providerPromptCacheKey?: string;
|
|
44
50
|
/** Skip flushing the current session and delete it instead of saving. */
|
|
45
51
|
drop?: boolean;
|
|
52
|
+
/** Additional workspace directories to seed on the new session. */
|
|
53
|
+
additionalDirectories?: string[];
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export interface SessionEntryBase {
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/** Handoff generation and session transition orchestration. */
|
|
2
|
+
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
type Agent,
|
|
6
|
+
type AgentMessage,
|
|
7
|
+
resolveTelemetry,
|
|
8
|
+
type StreamFn,
|
|
9
|
+
type ThinkingLevel,
|
|
10
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
11
|
+
import { generateHandoffFromContext, renderHandoffPrompt } from "@oh-my-pi/pi-agent-core/compaction";
|
|
12
|
+
import type { Message, Model, ServiceTier, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
|
|
13
|
+
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
15
|
+
import type { Settings } from "../config/settings";
|
|
16
|
+
import type { ExtensionRunner, SessionBeforeSwitchResult } from "../extensibility/extensions";
|
|
17
|
+
import { obfuscateProviderContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
18
|
+
import type { HandoffResult, SessionHandoffOptions } from "./agent-session-types";
|
|
19
|
+
import type { BashSessionTransition } from "./bash-runner";
|
|
20
|
+
import type { SessionContext } from "./session-context";
|
|
21
|
+
import type { SessionManager } from "./session-manager";
|
|
22
|
+
|
|
23
|
+
function createHandoffContext(document: string): string {
|
|
24
|
+
return `<handoff-context>\n${document}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createHandoffFileName(date = new Date()): string {
|
|
28
|
+
const fileTimestamp = date.toISOString().replace(/[:.]/g, "-");
|
|
29
|
+
return `handoff-${fileTimestamp}.md`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Capabilities borrowed from the owning AgentSession. */
|
|
33
|
+
export interface SessionHandoffHost {
|
|
34
|
+
agent: Agent;
|
|
35
|
+
sessionManager: SessionManager;
|
|
36
|
+
settings: Settings;
|
|
37
|
+
modelRegistry: ModelRegistry;
|
|
38
|
+
extensionRunner: ExtensionRunner | undefined;
|
|
39
|
+
sideStreamFn: StreamFn;
|
|
40
|
+
obfuscator: SecretObfuscator | undefined;
|
|
41
|
+
model(): Model | undefined;
|
|
42
|
+
thinkingLevel(): ThinkingLevel | undefined;
|
|
43
|
+
sessionId(): string;
|
|
44
|
+
sessionFile(): string | undefined;
|
|
45
|
+
baseSystemPrompt(): string[];
|
|
46
|
+
assertVibeSessionTransitionAllowed(action: string): void;
|
|
47
|
+
setSkipPostTurnMaintenance(timestamp: number | undefined): void;
|
|
48
|
+
obfuscateTextForProvider(text: string | undefined): string | undefined;
|
|
49
|
+
deobfuscateFromProvider(text: string): string;
|
|
50
|
+
convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]>;
|
|
51
|
+
prepareSimpleStreamOptions(options: SimpleStreamOptions, provider?: string): SimpleStreamOptions;
|
|
52
|
+
effectiveServiceTier(model: Model | undefined): ServiceTier | undefined;
|
|
53
|
+
flushPendingBash(): Promise<void>;
|
|
54
|
+
beginBashSessionTransition(): BashSessionTransition;
|
|
55
|
+
markBashSessionTransition(transition: BashSessionTransition): void;
|
|
56
|
+
finishBashSessionTransition(transition: BashSessionTransition, success: boolean): void;
|
|
57
|
+
cancelOwnAsyncJobs(): void;
|
|
58
|
+
clearCheckpointRuntimeState(): void;
|
|
59
|
+
clearSessionScopedToolState(): void;
|
|
60
|
+
clearFreshProviderSessionId(): void;
|
|
61
|
+
syncAgentSessionId(): void;
|
|
62
|
+
rekeyMemoryForCurrentSessionId(): void;
|
|
63
|
+
resetMemoryContextForNewTranscript(): Promise<void>;
|
|
64
|
+
clearPendingNextTurnMessages(): void;
|
|
65
|
+
resetTodoCycle(): void;
|
|
66
|
+
buildDisplaySessionContext(): SessionContext;
|
|
67
|
+
resetAdvisorRuntimes(): void;
|
|
68
|
+
syncTodoPhasesFromBranch(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Generates handoff documents and owns the handoff session transition. */
|
|
72
|
+
export class SessionHandoff {
|
|
73
|
+
#handoffAbortController: AbortController | undefined;
|
|
74
|
+
readonly #host: SessionHandoffHost;
|
|
75
|
+
|
|
76
|
+
constructor(host: SessionHandoffHost) {
|
|
77
|
+
this.#host = host;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Cancel in-progress handoff generation.
|
|
81
|
+
*/
|
|
82
|
+
abortHandoff(): void {
|
|
83
|
+
this.#handoffAbortController?.abort();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if handoff generation is in progress.
|
|
88
|
+
*/
|
|
89
|
+
get isGeneratingHandoff(): boolean {
|
|
90
|
+
return this.#handoffAbortController !== undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a handoff document with a oneshot LLM call, then start a new session with it.
|
|
95
|
+
*
|
|
96
|
+
* @param customInstructions Optional focus for the handoff document
|
|
97
|
+
* @param options Handoff execution options
|
|
98
|
+
* @returns The handoff document text, or undefined if cancelled/failed
|
|
99
|
+
*/
|
|
100
|
+
async handoff(customInstructions?: string, options?: SessionHandoffOptions): Promise<HandoffResult | undefined> {
|
|
101
|
+
this.#host.assertVibeSessionTransitionAllowed("handoff to a new session");
|
|
102
|
+
const entries = this.#host.sessionManager.getBranch();
|
|
103
|
+
const messageCount = entries.filter(e => e.type === "message").length;
|
|
104
|
+
|
|
105
|
+
if (messageCount < 2) {
|
|
106
|
+
throw new Error("Nothing to hand off (no messages yet)");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.#host.setSkipPostTurnMaintenance(undefined);
|
|
110
|
+
|
|
111
|
+
this.#handoffAbortController = new AbortController();
|
|
112
|
+
const handoffAbortController = this.#handoffAbortController;
|
|
113
|
+
const handoffSignal = handoffAbortController.signal;
|
|
114
|
+
const sourceSignal = options?.signal;
|
|
115
|
+
const onSourceAbort = () => {
|
|
116
|
+
if (!handoffSignal.aborted) {
|
|
117
|
+
handoffAbortController.abort();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
if (sourceSignal) {
|
|
121
|
+
sourceSignal.addEventListener("abort", onSourceAbort, { once: true });
|
|
122
|
+
if (sourceSignal.aborted) {
|
|
123
|
+
onSourceAbort();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (handoffSignal.aborted) {
|
|
129
|
+
throw new Error("Handoff cancelled");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const model = this.#host.model();
|
|
133
|
+
if (!model) {
|
|
134
|
+
throw new Error("No model selected for handoff");
|
|
135
|
+
}
|
|
136
|
+
const apiKey = await this.#host.modelRegistry.getApiKey(model, this.#host.sessionId());
|
|
137
|
+
if (!apiKey) {
|
|
138
|
+
throw new Error(`No API key for ${model.provider}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build the handoff request through the SAME pipeline a live turn uses
|
|
142
|
+
// (`runEphemeralTurn` / `/btw` share it) so the oneshot reads the
|
|
143
|
+
// provider prompt cache the main turn populated instead of cold-missing
|
|
144
|
+
// the whole prefix: identical system prompt, normalized tools, and
|
|
145
|
+
// transform-/obfuscation-matched message history via
|
|
146
|
+
// `convertMessagesToLlm` + `buildSideRequestContext`, plus the live turn's
|
|
147
|
+
// effective provider cache key with a unique side `sessionId` so
|
|
148
|
+
// OpenAI/Codex append-only state never mixes with the live turn.
|
|
149
|
+
const cacheSessionId = this.#host.sessionId();
|
|
150
|
+
// The loop sends `promptCacheKey` (providerPromptCacheKey) and falls back to
|
|
151
|
+
// the provider session id; providers route on `promptCacheKey ?? sessionId`.
|
|
152
|
+
// Both can diverge from this.#host.sessionId() (tan/subagent/shared sessions), so
|
|
153
|
+
// mirror exactly what the live turn populated the cache under.
|
|
154
|
+
const handoffPromptCacheKey = this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId;
|
|
155
|
+
const handoffPromptText = renderHandoffPrompt(this.#host.obfuscateTextForProvider(customInstructions));
|
|
156
|
+
const handoffSnapshot: AgentMessage[] = [
|
|
157
|
+
...this.#host.agent.state.messages,
|
|
158
|
+
{
|
|
159
|
+
role: "user",
|
|
160
|
+
content: [{ type: "text", text: handoffPromptText }],
|
|
161
|
+
attribution: "agent",
|
|
162
|
+
timestamp: Date.now(),
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
const handoffLlmMessages = await this.#host.convertMessagesToLlm(handoffSnapshot, handoffSignal);
|
|
166
|
+
// Base system prompt, not a per-turn `before_agent_start` hook override —
|
|
167
|
+
// the handoff seeds a fresh session and must not carry prompt-specific
|
|
168
|
+
// hook state. Matches the prompt the old handoff path sent.
|
|
169
|
+
const handoffContext = await this.#host.agent.buildSideRequestContext(
|
|
170
|
+
handoffLlmMessages,
|
|
171
|
+
this.#host.baseSystemPrompt(),
|
|
172
|
+
);
|
|
173
|
+
const handoffStreamOptions = this.#host.prepareSimpleStreamOptions(
|
|
174
|
+
{
|
|
175
|
+
apiKey: this.#host.modelRegistry.resolver(model, cacheSessionId),
|
|
176
|
+
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
177
|
+
promptCacheKey: handoffPromptCacheKey,
|
|
178
|
+
preferWebsockets: false,
|
|
179
|
+
serviceTier: this.#host.effectiveServiceTier(model),
|
|
180
|
+
hideThinkingSummary: this.#host.agent.hideThinkingSummary,
|
|
181
|
+
initiatorOverride: "agent",
|
|
182
|
+
signal: handoffSignal,
|
|
183
|
+
},
|
|
184
|
+
model.provider,
|
|
185
|
+
);
|
|
186
|
+
const rawHandoffText = await generateHandoffFromContext(
|
|
187
|
+
obfuscateProviderContext(this.#host.obfuscator, handoffContext),
|
|
188
|
+
model,
|
|
189
|
+
{
|
|
190
|
+
streamOptions: handoffStreamOptions,
|
|
191
|
+
completeImpl: async (requestModel, requestContext, requestOptions) => {
|
|
192
|
+
const stream = await this.#host.sideStreamFn(requestModel, requestContext, requestOptions);
|
|
193
|
+
return stream.result();
|
|
194
|
+
},
|
|
195
|
+
telemetry: resolveTelemetry(this.#host.agent.telemetry, this.#host.sessionId()),
|
|
196
|
+
// Honor the user's /model thinking selection on the handoff path.
|
|
197
|
+
// Clamped per-model inside generateHandoffFromContext via
|
|
198
|
+
// resolveCompactionEffort so unsupported-effort models don't trip
|
|
199
|
+
// requireSupportedEffort.
|
|
200
|
+
thinkingLevel: this.#host.thinkingLevel(),
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
const handoffText = this.#host.deobfuscateFromProvider(rawHandoffText);
|
|
204
|
+
|
|
205
|
+
if (handoffSignal.aborted) {
|
|
206
|
+
throw new Error("Handoff cancelled");
|
|
207
|
+
}
|
|
208
|
+
if (!handoffText) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Start a new session
|
|
213
|
+
const previousSessionFile = this.#host.sessionFile();
|
|
214
|
+
if (this.#host.extensionRunner?.hasHandlers("session_before_switch")) {
|
|
215
|
+
const result = (await this.#host.extensionRunner.emit({
|
|
216
|
+
type: "session_before_switch",
|
|
217
|
+
reason: "handoff",
|
|
218
|
+
})) as SessionBeforeSwitchResult | undefined;
|
|
219
|
+
|
|
220
|
+
if (result?.cancel) {
|
|
221
|
+
options?.onSwitchCancelled?.();
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
await this.#host.flushPendingBash();
|
|
226
|
+
await this.#host.sessionManager.flush();
|
|
227
|
+
const bashTransition = this.#host.beginBashSessionTransition();
|
|
228
|
+
this.#host.cancelOwnAsyncJobs();
|
|
229
|
+
let sessionTransitioned = false;
|
|
230
|
+
try {
|
|
231
|
+
await this.#host.sessionManager.newSession(
|
|
232
|
+
previousSessionFile ? { parentSession: previousSessionFile } : undefined,
|
|
233
|
+
);
|
|
234
|
+
this.#host.markBashSessionTransition(bashTransition);
|
|
235
|
+
sessionTransitioned = true;
|
|
236
|
+
} finally {
|
|
237
|
+
this.#host.finishBashSessionTransition(bashTransition, sessionTransitioned);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.#host.clearSessionScopedToolState();
|
|
241
|
+
|
|
242
|
+
this.#host.clearCheckpointRuntimeState();
|
|
243
|
+
// agent.reset() clears the core steering/follow-up queues. Preserve any queued
|
|
244
|
+
// steers/follow-ups (RPC/SDK steer()/followUp() issued during the handoff, or a
|
|
245
|
+
// pre-loader TUI steer) so they survive into the post-handoff session instead of
|
|
246
|
+
// being silently dropped. Capture is synchronous immediately before reset and
|
|
247
|
+
// restore is synchronous immediately after — no await gap — so a steer arriving
|
|
248
|
+
// later (during ensureOnDisk/Bun.write below) appends to the restored queue
|
|
249
|
+
// rather than being clobbered.
|
|
250
|
+
const preservedSteering = this.#host.agent.peekSteeringQueue().slice();
|
|
251
|
+
const preservedFollowUp = this.#host.agent.peekFollowUpQueue().slice();
|
|
252
|
+
this.#host.agent.reset();
|
|
253
|
+
this.#host.agent.replaceQueues(preservedSteering, preservedFollowUp);
|
|
254
|
+
this.#host.clearFreshProviderSessionId();
|
|
255
|
+
this.#host.syncAgentSessionId();
|
|
256
|
+
this.#host.rekeyMemoryForCurrentSessionId();
|
|
257
|
+
await this.#host.resetMemoryContextForNewTranscript();
|
|
258
|
+
this.#host.clearPendingNextTurnMessages();
|
|
259
|
+
this.#host.resetTodoCycle();
|
|
260
|
+
|
|
261
|
+
// Inject the handoff document as a custom message
|
|
262
|
+
const handoffContent = createHandoffContext(handoffText);
|
|
263
|
+
this.#host.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
|
|
264
|
+
await this.#host.sessionManager.ensureOnDisk();
|
|
265
|
+
let savedPath: string | undefined;
|
|
266
|
+
if (options?.autoTriggered && this.#host.settings.get("compaction.handoffSaveToDisk")) {
|
|
267
|
+
const artifactsDir = this.#host.sessionManager.getArtifactsDir();
|
|
268
|
+
if (artifactsDir) {
|
|
269
|
+
const handoffFilePath = path.join(artifactsDir, createHandoffFileName());
|
|
270
|
+
try {
|
|
271
|
+
await Bun.write(handoffFilePath, `${handoffText}\n`);
|
|
272
|
+
savedPath = handoffFilePath;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
logger.warn("Failed to save handoff document to disk", {
|
|
275
|
+
path: handoffFilePath,
|
|
276
|
+
error: error instanceof Error ? error.message : String(error),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
logger.debug("Skipping handoff document save because session is not persisted");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Rebuild agent messages from session
|
|
285
|
+
const sessionContext = this.#host.buildDisplaySessionContext();
|
|
286
|
+
this.#host.agent.replaceMessages(sessionContext.messages);
|
|
287
|
+
this.#host.resetAdvisorRuntimes();
|
|
288
|
+
this.#host.syncTodoPhasesFromBranch();
|
|
289
|
+
if (this.#host.extensionRunner) {
|
|
290
|
+
await this.#host.extensionRunner.emit({
|
|
291
|
+
type: "session_switch",
|
|
292
|
+
reason: "handoff",
|
|
293
|
+
previousSessionFile,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { document: handoffText, savedPath };
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (handoffSignal.aborted || (error instanceof Error && error.name === "AbortError")) {
|
|
300
|
+
throw new Error("Handoff cancelled");
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
} finally {
|
|
304
|
+
sourceSignal?.removeEventListener("abort", onSourceAbort);
|
|
305
|
+
this.#handoffAbortController = undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|