@oh-my-pi/pi-coding-agent 16.1.15 → 16.1.16
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 +48 -0
- package/dist/cli.js +3725 -4029
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +12 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +15 -0
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/session-picker.ts +17 -3
- package/src/commands/launch.ts +3 -3
- package/src/config/settings-schema.ts +13 -1
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/controllers/command-controller.ts +5 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +16 -0
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/todo.md +1 -1
- package/src/session/agent-session.ts +231 -50
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +20 -0
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/web/search/providers/exa.ts +85 -1
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
Run code in a persistent kernel
|
|
1
|
+
Run one step of code in a persistent kernel.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
|
|
4
|
+
**One eval call = one cell = one logical step.** State persists per language across separate eval calls, tool calls, and `task` subagents — define helpers/datasets/clients in one call, then later calls reuse them directly.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Work incrementally: imports in one call, define in the next, test, then use — each its own eval call. Re-run setup ONLY after `reset`, a kernel crash, or a `NameError`/`ReferenceError` proving the state is gone. Parallelize work *within* a cell with the `parallel(thunks)` helper, not by batching steps.
|
|
7
|
+
|
|
8
|
+
Fields:
|
|
7
9
|
|
|
8
10
|
- `language` — {{#if py}}`"py"` IPython kernel{{/if}}{{#ifAll py js}}, {{/ifAll}}{{#if js}}`"js"` persistent JavaScript VM{{/if}}{{#if rb}}{{#ifAny py js}}, {{/ifAny}}`"rb"` persistent Ruby kernel{{/if}}{{#if jl}}{{#ifAny py js rb}}, {{/ifAny}}`"jl"` persistent Julia kernel{{/if}}.
|
|
9
11
|
- `code` — cell body, verbatim. Newlines/quotes JSON-encoded; no fences, no headers.
|
|
10
12
|
- `title` (optional) — short transcript label (e.g. `"imports"`).
|
|
11
|
-
- `timeout` (optional) —
|
|
12
|
-
- `reset` (optional) — wipe this
|
|
13
|
+
- `timeout` (optional) — seconds. Raise only for heavy compute or long non-agent tool calls.
|
|
14
|
+
- `reset` (optional) — wipe this language's kernel first.{{#ifAll py js}} Per-language: a `py` reset never touches the JS VM.{{/ifAll}}
|
|
13
15
|
|
|
14
|
-
Work incrementally — one logical step per cell (imports, define, test, use), many small cells per call; workflow notes in the assistant message or `title`, never in cell code.
|
|
15
16
|
{{#if py}}Live event loop: use top-level `await` directly; `asyncio.run(…)` raises "cannot be called from a running event loop".{{/if}}
|
|
16
|
-
{{#if
|
|
17
|
-
{{#if
|
|
18
|
-
|
|
17
|
+
{{#if js}}JS runs under **Bun**: Bun globals/APIs are available (`Bun.file`, `Bun.write`, `Bun.$`, `fetch`, `Buffer`); top-level `await`/`return` work directly.{{/if}}
|
|
18
|
+
{{#if rb}}Ruby: synchronous; helper options are keyword args (e.g. `output("id", limit: 2)`); the last expression auto-displays unless it is `nil`, an assignment, or a definition (like IRB).{{/if}}
|
|
19
|
+
{{#if jl}}Julia: synchronous; helper options are standard keyword args (e.g. `output("id", limit=2)`); the last expression auto-displays unless it is an assignment or a definition (like the Julia REPL).{{/if}}
|
|
20
|
+
On error, fix and re-run only the failing step — prior calls' state survives.
|
|
19
21
|
</instruction>
|
|
20
22
|
|
|
21
23
|
<prelude>
|
|
@@ -29,12 +31,6 @@ read(path, offset?=1, limit?=None) → str
|
|
|
29
31
|
File as text; offset/limit 1-indexed lines. Accepts `local://…`.
|
|
30
32
|
write(path, content) → str
|
|
31
33
|
Write file (creates parents) → resolved path. `local://…` persists across turns/subagents.
|
|
32
|
-
append(path, content) → str
|
|
33
|
-
Append → resolved path. Accepts `local://…`.
|
|
34
|
-
tree(path?=".", max_depth?=3, show_hidden?=False) → str
|
|
35
|
-
Directory tree.
|
|
36
|
-
diff(a, b) → str
|
|
37
|
-
Unified diff of two files.
|
|
38
34
|
env(key?=None, value?=None) → str | None | dict
|
|
39
35
|
No args → full env dict; one → value of `key`; two → set `key=value`, return value.
|
|
40
36
|
output(*ids, format?="raw", query?=None, offset?=None, limit?=None) → str | dict | list[dict]
|
|
@@ -43,9 +39,9 @@ tool.<name>(args) → unknown
|
|
|
43
39
|
Invoke any session tool; `args` = its parameter object.
|
|
44
40
|
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
45
41
|
Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → structured output, parsed object.
|
|
46
|
-
{{#if spawns}}agent(prompt,
|
|
47
|
-
Run a subagent → final output. `
|
|
48
|
-
{{#if js}} JS: options are ONE trailing object — agent(prompt, {
|
|
42
|
+
{{#if spawns}}agent(prompt, agent?="task", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
43
|
+
Run a subagent → final output. `agent` picks another discovered agent; `schema` as in completion(). Background via `local://` files named in the prompt. `handle` → DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed under `data` when `schema` set).
|
|
44
|
+
{{#if js}} JS: options are ONE trailing object — agent(prompt, { agent, schema, handle }).
|
|
49
45
|
{{/if}}
|
|
50
46
|
{{/if}}
|
|
51
47
|
parallel(thunks) → list
|
|
@@ -63,10 +59,14 @@ budget → per-turn token budget
|
|
|
63
59
|
{{#if spawns}}
|
|
64
60
|
<dag>
|
|
65
61
|
Pipe handles through stage helpers to build a dependency graph — acyclic waves:
|
|
66
|
-
- **Name nodes.** Capture each `agent(…, {{#if py}}
|
|
62
|
+
- **Name nodes.** Capture each `agent(…, {{#if py}}handle=True{{/if}}{{#if js}}{ handle: true }{{/if}}{{#if jl}}handle=true{{/if}})` result; carries `handle` (`agent://<id>`) + `output`.
|
|
67
63
|
- **Wire edges by reference.** Put an upstream node's `handle`/`output` in the dependent stage's prompt — large transcript never re-inlined. Bulk: `write("local://<name>.md", …)`, pass the URI.
|
|
68
64
|
- **`pipeline(items, *stages)` = staged waves**, barrier between stages (every item clears stage N before any enters N+1). **`parallel(thunks)` = one wave** of independent nodes.
|
|
69
65
|
- **Isolate failure.** A raising node re-raises the lowest-index error, aborts its wave; wrap risky nodes in try/except so a failure degrades only its dependent subtree, independent branches finish.
|
|
70
66
|
- **Acyclic only.** A node never waits on its own descendant.
|
|
71
67
|
</dag>
|
|
72
68
|
{{/if}}
|
|
69
|
+
|
|
70
|
+
<critical>
|
|
71
|
+
Prior top-level names (`data`, `sessions`, helpers, imports) survive into the next eval call — reuse them; NEVER re-import, re-require, or re-declare a helper. Re-read a file only if it may have changed since the last read. Re-run setup only after `reset`, a crash, or a `NameError`/`ReferenceError`.
|
|
72
|
+
</critical>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
**Tasks referenced by verbatim content string, NEVER an auto-generated ID — no "task-1"/"task-N" exists. Pass the content text in the `task` field.**
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Next pending task auto-promotes to `in_progress` on each completion.
|
|
4
4
|
|
|
5
5
|
## Operations
|
|
6
6
|
|
|
@@ -55,8 +55,9 @@ import {
|
|
|
55
55
|
effectiveReserveTokens,
|
|
56
56
|
estimateTokens,
|
|
57
57
|
generateBranchSummary,
|
|
58
|
-
|
|
58
|
+
generateHandoffFromContext,
|
|
59
59
|
prepareCompaction,
|
|
60
|
+
renderHandoffPrompt,
|
|
60
61
|
resolveThresholdTokens,
|
|
61
62
|
type SessionEntry,
|
|
62
63
|
type SessionMessageEntry,
|
|
@@ -103,7 +104,7 @@ import {
|
|
|
103
104
|
resolveServiceTier,
|
|
104
105
|
streamSimple,
|
|
105
106
|
} from "@oh-my-pi/pi-ai";
|
|
106
|
-
import {
|
|
107
|
+
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
107
108
|
import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
108
109
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
109
110
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
@@ -1619,6 +1620,7 @@ export class AgentSession {
|
|
|
1619
1620
|
await this.#advisorRuntime.waitForCatchup(30000, threshold, signal);
|
|
1620
1621
|
}
|
|
1621
1622
|
}
|
|
1623
|
+
await this.#maintainContextMidRun(messages, signal);
|
|
1622
1624
|
});
|
|
1623
1625
|
this.yieldQueue = new YieldQueue({
|
|
1624
1626
|
isStreaming: () => this.isStreaming,
|
|
@@ -2777,10 +2779,32 @@ export class AgentSession {
|
|
|
2777
2779
|
this.#lastAssistantMessage = undefined;
|
|
2778
2780
|
if (!msg) {
|
|
2779
2781
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2782
|
+
logger.debug("agent_end maintenance routing", {
|
|
2783
|
+
reason: "no-assistant-message",
|
|
2784
|
+
goalModeEnabled: this.#goalModeState?.enabled === true,
|
|
2785
|
+
goalStatus: this.#goalModeState?.goal.status,
|
|
2786
|
+
});
|
|
2780
2787
|
await emitAgentEndNotification();
|
|
2781
2788
|
return;
|
|
2782
2789
|
}
|
|
2783
2790
|
|
|
2791
|
+
const maintenanceRoute = (route: string, extra?: Record<string, unknown>) => {
|
|
2792
|
+
logger.debug("agent_end maintenance routing", {
|
|
2793
|
+
route,
|
|
2794
|
+
stopReason: msg.stopReason,
|
|
2795
|
+
provider: msg.provider,
|
|
2796
|
+
model: msg.model,
|
|
2797
|
+
contentBlocks: msg.content.length,
|
|
2798
|
+
hasToolCalls: msg.content.some(content => content.type === "toolCall"),
|
|
2799
|
+
hasText: msg.content.some(content => content.type === "text"),
|
|
2800
|
+
goalModeEnabled: this.#goalModeState?.enabled === true,
|
|
2801
|
+
goalStatus: this.#goalModeState?.goal.status,
|
|
2802
|
+
successfulYield: this.#assistantEndedWithSuccessfulYield(msg),
|
|
2803
|
+
...extra,
|
|
2804
|
+
});
|
|
2805
|
+
};
|
|
2806
|
+
maintenanceRoute("entered");
|
|
2807
|
+
|
|
2784
2808
|
// Invalidate GitHub Copilot credentials on auth failure so stale tokens
|
|
2785
2809
|
// aren't reused on the next request
|
|
2786
2810
|
if (
|
|
@@ -2794,27 +2818,61 @@ export class AgentSession {
|
|
|
2794
2818
|
if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
|
|
2795
2819
|
this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
|
|
2796
2820
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2821
|
+
maintenanceRoute("skip-post-turn-maintenance");
|
|
2797
2822
|
await emitAgentEndNotification();
|
|
2798
2823
|
return;
|
|
2799
2824
|
}
|
|
2800
2825
|
|
|
2826
|
+
const activeGoal = this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active";
|
|
2801
2827
|
if (this.#assistantEndedWithSuccessfulYield(msg)) {
|
|
2802
2828
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2803
|
-
if (
|
|
2829
|
+
if (activeGoal) {
|
|
2830
|
+
maintenanceRoute("successful-yield-active-goal-checkCompaction");
|
|
2804
2831
|
const compactionTask = this.#checkCompaction(msg);
|
|
2805
2832
|
this.#trackPostPromptTask(compactionTask);
|
|
2806
2833
|
await compactionTask;
|
|
2834
|
+
} else {
|
|
2835
|
+
maintenanceRoute("successful-yield-no-active-goal");
|
|
2807
2836
|
}
|
|
2808
2837
|
await emitAgentEndNotification();
|
|
2809
2838
|
return;
|
|
2810
2839
|
}
|
|
2811
2840
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2812
2841
|
|
|
2842
|
+
// Empty-stop cleanup MUST run before any compaction continuation: an
|
|
2843
|
+
// empty toolUse stop must be stripped from active context + session
|
|
2844
|
+
// history before we schedule another turn, otherwise the next
|
|
2845
|
+
// Anthropic turn carries a tool_use block with no matching
|
|
2846
|
+
// tool_result and corrupts message history. The handler also
|
|
2847
|
+
// schedules its own retry, so a real empty stop never needs the
|
|
2848
|
+
// active-goal threshold pre-empt below.
|
|
2813
2849
|
if (await this.#handleEmptyAssistantStop(msg)) {
|
|
2850
|
+
maintenanceRoute("empty-stop-handled");
|
|
2814
2851
|
await emitAgentEndNotification();
|
|
2815
2852
|
return;
|
|
2816
2853
|
}
|
|
2854
|
+
|
|
2855
|
+
let compactionResult = COMPACTION_CHECK_NONE;
|
|
2856
|
+
let checkedCompaction = false;
|
|
2857
|
+
if (activeGoal) {
|
|
2858
|
+
maintenanceRoute("active-goal-pre-empt-checkCompaction");
|
|
2859
|
+
const compactionTask = this.#checkCompaction(msg);
|
|
2860
|
+
this.#trackPostPromptTask(compactionTask);
|
|
2861
|
+
compactionResult = await compactionTask;
|
|
2862
|
+
checkedCompaction = true;
|
|
2863
|
+
if (compactionResult.deferredHandoff || compactionResult.continuationScheduled) {
|
|
2864
|
+
maintenanceRoute("active-goal-pre-empt-continuation-scheduled", {
|
|
2865
|
+
deferredHandoff: compactionResult.deferredHandoff,
|
|
2866
|
+
continuationScheduled: compactionResult.continuationScheduled,
|
|
2867
|
+
});
|
|
2868
|
+
this.#resolveRetry();
|
|
2869
|
+
await emitAgentEndNotification();
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2817
2874
|
if (await this.#handleUnexpectedAssistantStop(msg)) {
|
|
2875
|
+
maintenanceRoute("unexpected-stop-handled");
|
|
2818
2876
|
await emitAgentEndNotification();
|
|
2819
2877
|
return;
|
|
2820
2878
|
}
|
|
@@ -2854,9 +2912,12 @@ export class AgentSession {
|
|
|
2854
2912
|
}
|
|
2855
2913
|
this.#resolveRetry();
|
|
2856
2914
|
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2915
|
+
if (!checkedCompaction) {
|
|
2916
|
+
maintenanceRoute("bottom-checkCompaction");
|
|
2917
|
+
const compactionTask = this.#checkCompaction(msg);
|
|
2918
|
+
this.#trackPostPromptTask(compactionTask);
|
|
2919
|
+
compactionResult = await compactionTask;
|
|
2920
|
+
}
|
|
2860
2921
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
2861
2922
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
2862
2923
|
if (hasToolCalls) {
|
|
@@ -8049,27 +8110,60 @@ export class AgentSession {
|
|
|
8049
8110
|
throw new Error(`No API key for ${model.provider}`);
|
|
8050
8111
|
}
|
|
8051
8112
|
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8113
|
+
// Build the handoff request through the SAME pipeline a live turn uses
|
|
8114
|
+
// (`runEphemeralTurn` / `/btw` share it) so the oneshot reads the
|
|
8115
|
+
// provider prompt cache the main turn populated instead of cold-missing
|
|
8116
|
+
// the whole prefix: identical system prompt, normalized tools, and
|
|
8117
|
+
// transform-/obfuscation-matched message history via
|
|
8118
|
+
// `convertMessagesToLlm` + `buildSideRequestContext`, plus the live turn's
|
|
8119
|
+
// effective provider cache key with a unique side `sessionId` so
|
|
8120
|
+
// OpenAI/Codex append-only state never mixes with the live turn.
|
|
8121
|
+
const cacheSessionId = this.sessionId;
|
|
8122
|
+
// The loop sends `promptCacheKey` (providerPromptCacheKey) and falls back to
|
|
8123
|
+
// the provider session id; providers route on `promptCacheKey ?? sessionId`.
|
|
8124
|
+
// Both can diverge from this.sessionId (tan/subagent/shared sessions), so
|
|
8125
|
+
// mirror exactly what the live turn populated the cache under.
|
|
8126
|
+
const handoffPromptCacheKey = this.agent.promptCacheKey ?? this.agent.sessionId;
|
|
8127
|
+
const handoffPromptText = renderHandoffPrompt(this.#obfuscateTextForProvider(customInstructions));
|
|
8128
|
+
const handoffSnapshot: AgentMessage[] = [
|
|
8129
|
+
...this.agent.state.messages,
|
|
8056
8130
|
{
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8131
|
+
role: "user",
|
|
8132
|
+
content: [{ type: "text", text: handoffPromptText }],
|
|
8133
|
+
attribution: "agent",
|
|
8134
|
+
timestamp: Date.now(),
|
|
8135
|
+
},
|
|
8136
|
+
];
|
|
8137
|
+
const handoffLlmMessages = await this.convertMessagesToLlm(handoffSnapshot, handoffSignal);
|
|
8138
|
+
// Base system prompt, not a per-turn `before_agent_start` hook override —
|
|
8139
|
+
// the handoff seeds a fresh session and must not carry prompt-specific
|
|
8140
|
+
// hook state. Matches the prompt the old handoff path sent.
|
|
8141
|
+
const handoffContext = await this.agent.buildSideRequestContext(handoffLlmMessages, this.#baseSystemPrompt);
|
|
8142
|
+
const handoffStreamOptions = this.prepareSimpleStreamOptions(
|
|
8143
|
+
{
|
|
8144
|
+
apiKey: this.#modelRegistry.resolver(model, cacheSessionId),
|
|
8145
|
+
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
8146
|
+
promptCacheKey: handoffPromptCacheKey,
|
|
8147
|
+
preferWebsockets: false,
|
|
8148
|
+
serviceTier: this.#effectiveServiceTier(model),
|
|
8149
|
+
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
8063
8150
|
initiatorOverride: "agent",
|
|
8064
|
-
|
|
8151
|
+
signal: handoffSignal,
|
|
8152
|
+
},
|
|
8153
|
+
model.provider,
|
|
8154
|
+
);
|
|
8155
|
+
const rawHandoffText = await generateHandoffFromContext(
|
|
8156
|
+
obfuscateProviderContext(this.#obfuscator, handoffContext),
|
|
8157
|
+
model,
|
|
8158
|
+
{
|
|
8159
|
+
streamOptions: handoffStreamOptions,
|
|
8065
8160
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
8066
|
-
// Honor the user's /model thinking selection on the handoff
|
|
8067
|
-
//
|
|
8068
|
-
// resolveCompactionEffort so unsupported-effort models don't
|
|
8069
|
-
//
|
|
8161
|
+
// Honor the user's /model thinking selection on the handoff path.
|
|
8162
|
+
// Clamped per-model inside generateHandoffFromContext via
|
|
8163
|
+
// resolveCompactionEffort so unsupported-effort models don't trip
|
|
8164
|
+
// requireSupportedEffort.
|
|
8070
8165
|
thinkingLevel: this.thinkingLevel,
|
|
8071
8166
|
},
|
|
8072
|
-
handoffSignal,
|
|
8073
8167
|
);
|
|
8074
8168
|
const handoffText = this.#deobfuscateFromProvider(rawHandoffText);
|
|
8075
8169
|
|
|
@@ -8213,6 +8307,58 @@ export class AgentSession {
|
|
|
8213
8307
|
});
|
|
8214
8308
|
}
|
|
8215
8309
|
|
|
8310
|
+
/**
|
|
8311
|
+
* Compact active `/goal` runs that never settle to `agent_end`.
|
|
8312
|
+
*
|
|
8313
|
+
* Long autonomous goals can keep producing tool calls inside one agent run.
|
|
8314
|
+
* The post-turn `agent_end` threshold check never fires in that shape, so
|
|
8315
|
+
* context can grow until provider overflow. `onTurnEnd` is the safe boundary:
|
|
8316
|
+
* tool results for the just-finished turn are already paired in
|
|
8317
|
+
* `activeMessages`, the live array the agent loop reads before its next
|
|
8318
|
+
* model call. Run maintenance here and splice the compacted state back into
|
|
8319
|
+
* that array, mirroring [`AgentSession.#applyRewind`].
|
|
8320
|
+
*/
|
|
8321
|
+
async #maintainContextMidRun(activeMessages: AgentMessage[], signal?: AbortSignal): Promise<void> {
|
|
8322
|
+
if (signal?.aborted || this.#isDisposed || this.isCompacting || this.isGeneratingHandoff) return;
|
|
8323
|
+
if (!(this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active")) return;
|
|
8324
|
+
|
|
8325
|
+
const model = this.model;
|
|
8326
|
+
const contextWindow = model?.contextWindow ?? 0;
|
|
8327
|
+
if (contextWindow <= 0) return;
|
|
8328
|
+
|
|
8329
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
8330
|
+
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
8331
|
+
|
|
8332
|
+
const lastAssistant = [...activeMessages]
|
|
8333
|
+
.reverse()
|
|
8334
|
+
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
8335
|
+
if (!lastAssistant || lastAssistant.stopReason === "aborted" || lastAssistant.stopReason === "error") return;
|
|
8336
|
+
|
|
8337
|
+
const billedContextTokens = calculateContextTokens(lastAssistant.usage);
|
|
8338
|
+
const storedContextTokens = this.#estimateStoredContextTokens();
|
|
8339
|
+
const contextTokens = compactionContextTokens(billedContextTokens, storedContextTokens);
|
|
8340
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
|
|
8341
|
+
|
|
8342
|
+
const messagesBefore = activeMessages.length;
|
|
8343
|
+
await this.#runAutoCompaction("threshold", false, false, false, {
|
|
8344
|
+
autoContinue: false,
|
|
8345
|
+
suppressContinuation: true,
|
|
8346
|
+
triggerContextTokens: contextTokens,
|
|
8347
|
+
});
|
|
8348
|
+
|
|
8349
|
+
if (signal?.aborted) return;
|
|
8350
|
+
const compactedMessages = this.agent.state.messages;
|
|
8351
|
+
if (compactedMessages !== activeMessages) {
|
|
8352
|
+
activeMessages.splice(0, activeMessages.length, ...compactedMessages);
|
|
8353
|
+
}
|
|
8354
|
+
logger.debug("Mid-run goal compaction ran between tool-call turns", {
|
|
8355
|
+
contextTokens,
|
|
8356
|
+
contextWindow,
|
|
8357
|
+
strategy: compactionSettings.strategy,
|
|
8358
|
+
messagesBefore,
|
|
8359
|
+
messagesAfter: activeMessages.length,
|
|
8360
|
+
});
|
|
8361
|
+
}
|
|
8216
8362
|
/**
|
|
8217
8363
|
* Check if context maintenance or promotion is needed and run it.
|
|
8218
8364
|
* Called after agent_end and before prompt submission.
|
|
@@ -8338,28 +8484,58 @@ export class AgentSession {
|
|
|
8338
8484
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
8339
8485
|
if (assistantMessage.stopReason === "error") return COMPACTION_CHECK_NONE;
|
|
8340
8486
|
const pruneResult = await this.#pruneToolOutputs();
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
//
|
|
8349
|
-
//
|
|
8350
|
-
//
|
|
8351
|
-
//
|
|
8352
|
-
//
|
|
8353
|
-
|
|
8354
|
-
|
|
8487
|
+
const maintenanceTokensFreed = (supersedeResult?.tokensSaved ?? 0) + (pruneResult?.tokensSaved ?? 0);
|
|
8488
|
+
const assistantUsageContextTokens = calculateContextTokens(assistantMessage.usage);
|
|
8489
|
+
const storedContextTokens = this.#estimateStoredContextTokens();
|
|
8490
|
+
// Pruning frees bytes for the NEXT prompt; it does not change the size of
|
|
8491
|
+
// the prompt the LLM just billed for. Earlier revisions subtracted the
|
|
8492
|
+
// per-turn supersede/prune `tokensSaved` from the threshold input, which
|
|
8493
|
+
// let a long-running `/goal` session sit above `compaction.thresholdTokens`
|
|
8494
|
+
// indefinitely whenever per-turn pruning saved enough to drop the
|
|
8495
|
+
// post-prune estimate below the user-configured trigger — the visible
|
|
8496
|
+
// context (anchored to the same provider billing) still showed >threshold,
|
|
8497
|
+
// but `shouldCompact` no-op'd (#3174). Anchor the initial trigger on the
|
|
8498
|
+
// last turn's billed context tokens, floored by the post-prune
|
|
8499
|
+
// stored-conversation estimate so a payload-compression hook still can't
|
|
8500
|
+
// deflate the trigger.
|
|
8501
|
+
const contextTokens = compactionContextTokens(assistantUsageContextTokens, storedContextTokens);
|
|
8502
|
+
const postMaintenanceContextTokens = compactionContextTokens(
|
|
8503
|
+
Math.max(0, assistantUsageContextTokens - maintenanceTokensFreed),
|
|
8504
|
+
storedContextTokens,
|
|
8505
|
+
);
|
|
8506
|
+
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
8507
|
+
const shouldThresholdCompact = shouldCompact(contextTokens, contextWindow, compactionSettings);
|
|
8508
|
+
logger.debug("Auto-compaction threshold decision", {
|
|
8509
|
+
phase: "post-agent-end",
|
|
8510
|
+
goalModeEnabled: this.#goalModeState?.enabled === true,
|
|
8511
|
+
goalStatus: this.#goalModeState?.goal.status,
|
|
8512
|
+
stopReason: assistantMessage.stopReason,
|
|
8513
|
+
sameModel: sameModel === true,
|
|
8514
|
+
contextWindow,
|
|
8515
|
+
strategy: compactionSettings.strategy,
|
|
8516
|
+
thresholdTokens,
|
|
8517
|
+
assistantUsageContextTokens,
|
|
8518
|
+
storedContextTokens,
|
|
8519
|
+
resolvedContextTokens: contextTokens,
|
|
8520
|
+
postMaintenanceContextTokens,
|
|
8521
|
+
maintenanceTokensFreed,
|
|
8522
|
+
shouldCompact: shouldThresholdCompact,
|
|
8523
|
+
contextPromotionEnabled: this.settings.get("contextPromotion.enabled") === true,
|
|
8524
|
+
});
|
|
8525
|
+
if (shouldThresholdCompact) {
|
|
8355
8526
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
8356
8527
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
8357
8528
|
if (!promoted) {
|
|
8358
8529
|
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
8359
8530
|
autoContinue,
|
|
8360
|
-
triggerContextTokens:
|
|
8531
|
+
triggerContextTokens: postMaintenanceContextTokens,
|
|
8361
8532
|
});
|
|
8362
8533
|
}
|
|
8534
|
+
logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
|
|
8535
|
+
contextTokens,
|
|
8536
|
+
contextWindow,
|
|
8537
|
+
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
8538
|
+
});
|
|
8363
8539
|
}
|
|
8364
8540
|
return COMPACTION_CHECK_NONE;
|
|
8365
8541
|
}
|
|
@@ -9483,13 +9659,15 @@ export class AgentSession {
|
|
|
9483
9659
|
willRetry: boolean,
|
|
9484
9660
|
deferred = false,
|
|
9485
9661
|
allowDefer = true,
|
|
9486
|
-
options: { autoContinue?: boolean; triggerContextTokens?: number } = {},
|
|
9662
|
+
options: { autoContinue?: boolean; triggerContextTokens?: number; suppressContinuation?: boolean } = {},
|
|
9487
9663
|
): Promise<CompactionCheckResult> {
|
|
9488
9664
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
9489
9665
|
if (compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
|
|
9490
9666
|
if (reason !== "idle" && !compactionSettings.enabled) return COMPACTION_CHECK_NONE;
|
|
9491
9667
|
const generation = this.#promptGeneration;
|
|
9492
|
-
const
|
|
9668
|
+
const suppressContinuation = options.suppressContinuation === true;
|
|
9669
|
+
const shouldAutoContinue =
|
|
9670
|
+
!suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
9493
9671
|
// Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
|
|
9494
9672
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
9495
9673
|
// the oversized input still gets resolved.
|
|
@@ -9500,6 +9678,7 @@ export class AgentSession {
|
|
|
9500
9678
|
generation,
|
|
9501
9679
|
shouldAutoContinue,
|
|
9502
9680
|
options.triggerContextTokens,
|
|
9681
|
+
suppressContinuation,
|
|
9503
9682
|
);
|
|
9504
9683
|
if (outcome !== "fallback") return outcome;
|
|
9505
9684
|
}
|
|
@@ -9953,7 +10132,7 @@ export class AgentSession {
|
|
|
9953
10132
|
|
|
9954
10133
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
9955
10134
|
continuationScheduled = true;
|
|
9956
|
-
} else if (this.agent.hasQueuedMessages()) {
|
|
10135
|
+
} else if (!suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
9957
10136
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
9958
10137
|
// Kick the loop so queued messages are actually delivered.
|
|
9959
10138
|
this.#scheduleAgentContinue({
|
|
@@ -10013,6 +10192,7 @@ export class AgentSession {
|
|
|
10013
10192
|
generation: number,
|
|
10014
10193
|
autoContinue: boolean,
|
|
10015
10194
|
triggerContextTokens?: number,
|
|
10195
|
+
suppressContinuation = false,
|
|
10016
10196
|
): Promise<CompactionCheckResult | "fallback"> {
|
|
10017
10197
|
const action = "shake";
|
|
10018
10198
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -10043,15 +10223,16 @@ export class AgentSession {
|
|
|
10043
10223
|
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
10044
10224
|
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
10045
10225
|
//
|
|
10046
|
-
// #2275: the post-shake check MUST
|
|
10047
|
-
//
|
|
10048
|
-
//
|
|
10049
|
-
//
|
|
10050
|
-
//
|
|
10051
|
-
//
|
|
10052
|
-
//
|
|
10053
|
-
//
|
|
10054
|
-
//
|
|
10226
|
+
// #2275: the post-shake check MUST stay provider-anchored when caller
|
|
10227
|
+
// usage and local estimates diverge. The local estimator undercounts
|
|
10228
|
+
// thinking-signature payloads, so thinking-heavy sessions can read well
|
|
10229
|
+
// below the provider usage that fired the threshold. Prefer the caller's
|
|
10230
|
+
// context figure when supplied, then subtract shake's own savings and add
|
|
10231
|
+
// hysteresis (80% recovery band) so we don't oscillate at the boundary.
|
|
10232
|
+
// Threshold callers pass the provider-billed trigger after accounting for
|
|
10233
|
+
// any supersede/drop-useless pruning that already rewrote the next prompt;
|
|
10234
|
+
// without that pre-shake savings, shake can fall through to context-full
|
|
10235
|
+
// even though the post-prune history is already inside the recovery band.
|
|
10055
10236
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
10056
10237
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
10057
10238
|
let stillOverThreshold = false;
|
|
@@ -10111,7 +10292,7 @@ export class AgentSession {
|
|
|
10111
10292
|
}
|
|
10112
10293
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
10113
10294
|
continuationScheduled = true;
|
|
10114
|
-
} else if (this.agent.hasQueuedMessages()) {
|
|
10295
|
+
} else if (!suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
10115
10296
|
this.#scheduleAgentContinue({
|
|
10116
10297
|
delayMs: 100,
|
|
10117
10298
|
generation,
|