one-code-extension 0.5.0 → 0.6.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/README.md +1 -1
- package/docs/guide/differences-from-claude-code.md +9 -6
- package/docs/guide/installation.md +3 -4
- package/docs/guide/troubleshooting.md +1 -1
- package/extensions/auto-mode/denials.ts +3 -2
- package/extensions/background/cron.ts +127 -0
- package/extensions/background/index.ts +135 -47
- package/extensions/background/loop-fire.ts +39 -2
- package/extensions/background/switch-guard.ts +10 -6
- package/extensions/background/wakeup.ts +21 -2
- package/extensions/bash/index.ts +8 -30
- package/extensions/btw/index.ts +32 -18
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-context/index.ts +157 -172
- package/extensions/compaction/index.ts +11 -8
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +15 -14
- package/extensions/context-management/index.ts +11 -28
- package/extensions/effort/index.ts +70 -7
- package/extensions/file-tracker/index.ts +111 -44
- package/extensions/file-tracker/replay.ts +76 -49
- package/extensions/file-tracker/restore.ts +12 -9
- package/extensions/file-tracker/tracker.ts +14 -1
- package/extensions/hooks/index.ts +60 -76
- package/extensions/hooks/prompt-context.ts +59 -0
- package/extensions/hooks/trust.ts +13 -7
- package/extensions/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +17 -1
- package/extensions/lib/agent-loader.ts +3 -2
- package/extensions/lib/anthropic-auth.ts +98 -0
- package/extensions/lib/child-extensions.ts +14 -4
- package/extensions/lib/context-facts.ts +75 -56
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +152 -12
- package/extensions/lib/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +24 -6
- package/extensions/lib/model-catalog/huggingface.json +5 -1
- package/extensions/lib/model-catalog/models-dev.json +10535 -10299
- package/extensions/lib/model-catalog/openrouter.json +191 -393
- package/extensions/lib/notifications.ts +57 -17
- package/extensions/lib/pi-version.ts +25 -8
- package/extensions/lib/plan-mode-channels.ts +29 -7
- package/extensions/lib/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +3 -2
- package/extensions/lib/reminders.ts +327 -621
- package/extensions/lib/request-replay.ts +186 -78
- package/extensions/lib/side-call.ts +11 -0
- package/extensions/lib/subagent-handback.ts +0 -1
- package/extensions/lib/tool-additions.ts +2 -22
- package/extensions/lib/turn-budget-layout.ts +18 -2
- package/extensions/lsp/index.ts +6 -8
- package/extensions/mcp/announce.ts +63 -98
- package/extensions/mcp/index.ts +51 -90
- package/extensions/mcp/trust.ts +13 -6
- package/extensions/model-default/index.ts +86 -19
- package/extensions/permissions/index.ts +157 -48
- package/extensions/permissions/modes.ts +5 -0
- package/extensions/plan-mode/index.ts +142 -45
- package/extensions/plan-mode/run-state.ts +26 -0
- package/extensions/plugins/index.ts +4 -1
- package/extensions/powershell/index.ts +2 -0
- package/extensions/recap/index.ts +11 -14
- package/extensions/search-tools/index.ts +2 -0
- package/extensions/skill/announce.ts +30 -0
- package/extensions/skill/index.ts +65 -48
- package/extensions/skill/invoke.ts +0 -67
- package/extensions/subagents/agents.ts +30 -8
- package/extensions/subagents/fork-cache.ts +9 -3
- package/extensions/subagents/index.ts +180 -107
- package/extensions/subagents/outcome.ts +3 -0
- package/extensions/subagents/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +29 -2
- package/extensions/subagents/runs.ts +5 -0
- package/extensions/system-prompt/index.ts +40 -6
- package/extensions/system-prompt/template.ts +10 -1
- package/extensions/system-reminder/index.ts +174 -202
- package/extensions/tool-search/index.ts +241 -167
- package/extensions/workflow/agent-session.ts +4 -1
- package/extensions/workflow/index.ts +16 -4
- package/extensions/workflow/run-manager.ts +4 -0
- package/extensions/worktree/index.ts +50 -14
- package/package.json +5 -2
- package/extensions/lib/context-stack.ts +0 -81
- package/extensions/subagents/announce.ts +0 -69
- package/extensions/tool-search/announce.ts +0 -58
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* at creation, so the move is enforced on the tool_call hook: bash commands
|
|
8
8
|
* are prefixed with `cd`, relative paths resolve against the worktree (see
|
|
9
9
|
* rewrite.ts), and an every-turn reminder keeps the model oriented. State
|
|
10
|
-
* rides in tool-result details so a resumed or branched session restores it
|
|
10
|
+
* rides in tool-result details so a resumed or branched session restores it;
|
|
11
|
+
* a /clear or /new keeps the worktree, as Claude Code does, by recording it on
|
|
12
|
+
* the new branch.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { execFile } from "node:child_process";
|
|
@@ -34,6 +36,8 @@ import { rewriteToolInput, validateWorktreeName } from "./rewrite.ts";
|
|
|
34
36
|
import { ccToolRenderers } from "../lib/tui-render.ts";
|
|
35
37
|
import { findProjectRoot, HARNESS_GIT_CONFIG, HARNESS_GIT_NO_HOOKS, repositoryWorktrees } from "../lib/git.ts";
|
|
36
38
|
import { projectConfigDir, projectConfigDirName } from "../lib/config-mode.ts";
|
|
39
|
+
import { readState, readStringsState, recordState } from "../lib/ledger/state.ts";
|
|
40
|
+
import { processState } from "../lib/process-state.ts";
|
|
37
41
|
|
|
38
42
|
const run = promisify(execFile);
|
|
39
43
|
const REMINDER_KEY = "cc-worktree-session";
|
|
@@ -74,12 +78,24 @@ async function listWorktreePaths(cwd: string): Promise<string[]> {
|
|
|
74
78
|
*/
|
|
75
79
|
const ISOLATION_NOTE = "Git commands aimed at the main checkout or another worktree of this repository are refused until you exit.";
|
|
76
80
|
|
|
81
|
+
/** The `one-code:state` key of the worktree paths the branch was already told are gone. */
|
|
82
|
+
const MISSING_REPORTED_STATE = "worktree:missing-reported";
|
|
83
|
+
|
|
84
|
+
/** The `one-code:state` key of the worktree a /clear or /new carried onto its new branch. */
|
|
85
|
+
const CARRIED_STATE = "worktree:carried";
|
|
86
|
+
|
|
77
87
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
88
|
+
* The worktree the session is in, as process state (lib/process-state.ts): a
|
|
89
|
+
* /clear or /new starts an empty branch, and Claude Code keeps the session in
|
|
90
|
+
* its worktree across that (decisions/tools.md). Children never load this
|
|
91
|
+
* extension, so only the main session writes it.
|
|
81
92
|
*/
|
|
82
|
-
const
|
|
93
|
+
const liveWorktree = () => processState<{ state?: WorktreeState }>("worktree:live", () => ({}));
|
|
94
|
+
|
|
95
|
+
function isWorktreeState(value: unknown): value is WorktreeState {
|
|
96
|
+
const s = value as Partial<WorktreeState> | null | undefined;
|
|
97
|
+
return typeof s?.path === "string" && typeof s.originalCwd === "string" && typeof s.sharedRoot === "string" && typeof s.createdByUs === "boolean";
|
|
98
|
+
}
|
|
83
99
|
|
|
84
100
|
export default function worktreeExtension(pi: ExtensionAPI) {
|
|
85
101
|
let state: WorktreeState | undefined;
|
|
@@ -91,12 +107,12 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
91
107
|
|
|
92
108
|
const applyState = (next: WorktreeState | undefined, toolCallId?: string) => {
|
|
93
109
|
state = next;
|
|
110
|
+
liveWorktree().state = next;
|
|
94
111
|
const location: WorktreeLocation | null = next ? { path: next.path, branch: next.branch, sharedRoot: next.sharedRoot } : null;
|
|
95
112
|
pi.events.emit(WORKTREE_CHANNEL, location);
|
|
96
113
|
if (next) {
|
|
97
114
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
98
115
|
text: reminderFor(next),
|
|
99
|
-
scope: "every-turn",
|
|
100
116
|
key: REMINDER_KEY,
|
|
101
117
|
placement: "sticky-append",
|
|
102
118
|
toolCallId,
|
|
@@ -106,15 +122,35 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
106
122
|
}
|
|
107
123
|
};
|
|
108
124
|
|
|
109
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The worktree a /clear or /new keeps: the one this process was in, when it
|
|
127
|
+
* belongs to this directory and still exists, recorded on the new branch so
|
|
128
|
+
* a resume of it reads it back. A branch with its own worktree tool results
|
|
129
|
+
* follows them instead (they come after the carry).
|
|
130
|
+
*/
|
|
131
|
+
const carriedState = (ctx: ExtensionContext, reason: string | undefined): WorktreeState | undefined => {
|
|
132
|
+
// A recorded carry holds only in the directory it was entered from, as
|
|
133
|
+
// the live one does: a branch forked into another project, or continued
|
|
134
|
+
// after its directory went away, starts outside the worktree.
|
|
135
|
+
const enteredHere = (s: WorktreeState) => comparablePath(tryRealpath(s.originalCwd) ?? s.originalCwd) === comparablePath(tryRealpath(ctx.cwd) ?? ctx.cwd);
|
|
136
|
+
const recorded = readState(ctx, CARRIED_STATE);
|
|
137
|
+
if (isWorktreeState(recorded)) return enteredHere(recorded) ? recorded : undefined;
|
|
138
|
+
const live = liveWorktree().state;
|
|
139
|
+
if (reason !== "new" || !live || !existsSync(live.path) || !enteredHere(live)) return undefined;
|
|
140
|
+
recordState(pi, CARRIED_STATE, live);
|
|
141
|
+
return live;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const reconstructState = (ctx: ExtensionContext, reason?: string) => {
|
|
110
145
|
const details = restoreLatestDetails<WorktreeDetails>(ctx.sessionManager.getBranch(), WORKTREE_TOOLS, (d) => d?.worktreeState !== undefined);
|
|
111
|
-
let restored = details
|
|
146
|
+
let restored = details ? (details.worktreeState ?? undefined) : carriedState(ctx, reason);
|
|
112
147
|
if (restored && !existsSync(restored.path)) {
|
|
113
|
-
// Nothing records that the session left, so every
|
|
114
|
-
// the same stale state; tell the
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
// Nothing in the tool details records that the session left, so every
|
|
149
|
+
// later restore finds the same stale state; tell the branch once, and
|
|
150
|
+
// record that it was told (a resume, /reload or /tree reads it back).
|
|
151
|
+
const reported = readStringsState(ctx, MISSING_REPORTED_STATE);
|
|
152
|
+
if (!reported.includes(restored.path)) {
|
|
153
|
+
recordState(pi, MISSING_REPORTED_STATE, [...reported, restored.path]);
|
|
118
154
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
119
155
|
text: `Left worktree session; back in ${restored.originalCwd}. The worktree at ${restored.path} no longer exists.`,
|
|
120
156
|
});
|
|
@@ -126,7 +162,7 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
126
162
|
applyState(restored);
|
|
127
163
|
};
|
|
128
164
|
|
|
129
|
-
pi.on("session_start", (
|
|
165
|
+
pi.on("session_start", (event, ctx) => reconstructState(ctx, event.reason));
|
|
130
166
|
pi.on("session_tree", (_event, ctx) => reconstructState(ctx));
|
|
131
167
|
|
|
132
168
|
/** The repository's worktrees for a git command's target check; undefined when git cannot list them. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "one-code-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "One Code as a pi package — the Claude Code experience on your own pi install: ported system prompt, Claude-Code-format permissions, .claude/ compatibility, ToolSearch-style deferred tools, and system-reminder steering. For the bundled app, install `@one-ai/one-code` instead.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"extensions/branding/index.ts",
|
|
30
30
|
"extensions/claude-compat/index.ts",
|
|
31
31
|
"extensions/kept-thinking/index.ts",
|
|
32
|
+
"extensions/ledger/index.ts",
|
|
32
33
|
"extensions/context-budget/index.ts",
|
|
33
34
|
"extensions/system-reminder/index.ts",
|
|
34
35
|
"extensions/claude-context/index.ts",
|
|
@@ -75,6 +76,8 @@
|
|
|
75
76
|
"extensions/context-management/index.ts",
|
|
76
77
|
"extensions/compaction/index.ts",
|
|
77
78
|
"extensions/idle-compact/index.ts",
|
|
79
|
+
"extensions/ledger/wire.ts",
|
|
80
|
+
"extensions/cache-marks/index.ts",
|
|
78
81
|
"extensions/system-prompt/index.ts"
|
|
79
82
|
],
|
|
80
83
|
"themes": [
|
|
@@ -120,7 +123,7 @@
|
|
|
120
123
|
"vitest": "^2.1.0"
|
|
121
124
|
},
|
|
122
125
|
"peerDependencies": {
|
|
123
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
126
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
124
127
|
"typebox": "*"
|
|
125
128
|
}
|
|
126
129
|
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/** Session-file context snapshots and the bus contract. No stored value grants permissions. */
|
|
2
|
-
import type { ReminderEntry } from "./reminders.ts";
|
|
3
|
-
|
|
4
|
-
export const CONTEXT_STACK_ENTRY = "one-code:context-stack";
|
|
5
|
-
export const CONTEXT_STATE_ENTRY = "one-code:context-state";
|
|
6
|
-
export const CONTEXT_RESTORE_CHANNEL = "one-code:context-restore";
|
|
7
|
-
export const CONTEXT_BASELINE_CHANNEL = "one-code:context-baseline";
|
|
8
|
-
/** A completed compaction replaces the Claude context facts, including absent blocks. */
|
|
9
|
-
export const CONTEXT_FACTS_REFRESH_CHANNEL = "one-code:context-facts-refresh";
|
|
10
|
-
|
|
11
|
-
export interface ContextStackSnapshot {
|
|
12
|
-
version: 1;
|
|
13
|
-
stack: ReminderEntry[];
|
|
14
|
-
sticky: ReminderEntry[];
|
|
15
|
-
/** Delivered one-shots/breadcrumbs; absent in older snapshots. */
|
|
16
|
-
pinned?: ReminderEntry[];
|
|
17
|
-
/** Emitter-owned announcement baselines, not live configuration. */
|
|
18
|
-
baselines: Record<string, unknown>;
|
|
19
|
-
}
|
|
20
|
-
export interface ContextRestoreRequest { restored?: ContextStackSnapshot }
|
|
21
|
-
export interface ContextBaseline { key: string; value: unknown }
|
|
22
|
-
export interface ContextFactsRefresh { entries: ReminderEntry[]; baseline: unknown }
|
|
23
|
-
interface Bus { emit(channel: string, data: unknown): void }
|
|
24
|
-
|
|
25
|
-
/** Synchronous request/reply: system-reminder restores before the emitters' session_start hooks. */
|
|
26
|
-
export function restoredContext(events: Bus): ContextStackSnapshot | undefined {
|
|
27
|
-
const request: ContextRestoreRequest = {};
|
|
28
|
-
events.emit(CONTEXT_RESTORE_CHANNEL, request);
|
|
29
|
-
return request.restored;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Phase-1 capabilities must be re-emitted by their live owners, including removal. */
|
|
33
|
-
export const LIVE_CONTEXT_KEYS = new Set(["environment", "model-line", "one-shot", "skills"]);
|
|
34
|
-
|
|
35
|
-
/** Session facts and phase-2 emitters keep even an absent original block absent. */
|
|
36
|
-
export const RESTORED_STACK_KEYS = new Set([
|
|
37
|
-
"claude-context", "one-code-context", "claude-context-context", "claude-context-date",
|
|
38
|
-
"total-tokens-first", "auto-mode-note", "deferred-tools", "mcp-instructions", "mcp-failures",
|
|
39
|
-
"subagent-models", "subagent-agents", "subagent-delegation",
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
43
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
44
|
-
}
|
|
45
|
-
function anchor(value: unknown): boolean {
|
|
46
|
-
return record(value) && ((value.kind === "user" && typeof value.timestamp === "number" && Number.isFinite(value.timestamp)) ||
|
|
47
|
-
(value.kind === "toolResult" && typeof value.toolCallId === "string"));
|
|
48
|
-
}
|
|
49
|
-
function entries(value: unknown, placement: "first-prepend" | "sticky-append" | "pinned"): value is ReminderEntry[] {
|
|
50
|
-
return Array.isArray(value) && value.every((e) => record(e) &&
|
|
51
|
-
(placement === "pinned" ? e.placement === "last-append" || e.placement === "user-prepend" : e.placement === placement) && typeof e.text === "string" &&
|
|
52
|
-
typeof e.order === "number" && Number.isFinite(e.order) &&
|
|
53
|
-
(e.key === undefined || typeof e.key === "string") && (e.suffix === undefined || typeof e.suffix === "string") &&
|
|
54
|
-
(e.toolCallId === undefined || placement === "sticky-append" && typeof e.toolCallId === "string") &&
|
|
55
|
-
["raw", "systemRoleOnly", "skipStackCarrier"].every((key) => e[key] === undefined || typeof e[key] === "boolean") &&
|
|
56
|
-
(e.since === undefined || typeof e.since === "number" && Number.isFinite(e.since)) &&
|
|
57
|
-
(e.until === undefined || placement === "sticky-append" && typeof e.until === "number" && Number.isFinite(e.until)) &&
|
|
58
|
-
(e.userPins === undefined || placement === "sticky-append" && Array.isArray(e.userPins) && e.userPins.every((stamp) => typeof stamp === "number" && Number.isFinite(stamp))) &&
|
|
59
|
-
(e.opener === undefined || e.opener === null || typeof e.opener === "number" && Number.isFinite(e.opener)) &&
|
|
60
|
-
(e.tailPin === undefined || anchor(e.tailPin)) && (placement === "pinned" ? anchor(e.pin) : e.pin === undefined));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Read only the selected branch. Malformed/unknown versions are ignored, never partly restored. */
|
|
64
|
-
export function contextStackOnBranch(branch: readonly { type: string; customType?: string; data?: unknown }[]): ContextStackSnapshot | undefined {
|
|
65
|
-
let snapshot: ContextStackSnapshot | undefined;
|
|
66
|
-
for (const entry of branch) {
|
|
67
|
-
if (entry.type !== "custom" || !record(entry.data) || entry.data.version !== 1) continue;
|
|
68
|
-
const data = entry.data;
|
|
69
|
-
if (data.pinned !== undefined && !entries(data.pinned, "pinned")) continue;
|
|
70
|
-
if (entry.customType === CONTEXT_STACK_ENTRY && entries(data.stack, "first-prepend") && entries(data.sticky, "sticky-append") && record(data.baselines)) {
|
|
71
|
-
snapshot = structuredClone(data) as unknown as ContextStackSnapshot;
|
|
72
|
-
} else if (snapshot && entry.customType === CONTEXT_STATE_ENTRY && entries(data.sticky, "sticky-append") && record(data.baselines) &&
|
|
73
|
-
(data.stack === undefined || entries(data.stack, "first-prepend"))) {
|
|
74
|
-
if (data.stack !== undefined) snapshot.stack = structuredClone(data.stack) as ReminderEntry[];
|
|
75
|
-
snapshot.sticky = structuredClone(data.sticky);
|
|
76
|
-
if (data.pinned !== undefined) snapshot.pinned = structuredClone(data.pinned) as ReminderEntry[];
|
|
77
|
-
snapshot.baselines = structuredClone(data.baselines);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return snapshot;
|
|
81
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/** Pure state for resumed-session subagent capability announcements. */
|
|
2
|
-
|
|
3
|
-
export type SubagentCapability = "models" | "agents" | "delegation";
|
|
4
|
-
export interface SubagentBaseline {
|
|
5
|
-
models: string | null;
|
|
6
|
-
agents: string | null;
|
|
7
|
-
delegation: string | null;
|
|
8
|
-
/** Account refusals the narrow tail correction has already communicated. */
|
|
9
|
-
refusedModels: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function emptySubagentBaseline(): SubagentBaseline {
|
|
13
|
-
return { models: null, agents: null, delegation: null, refusedModels: [] };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Stored baseline validation deliberately permits arbitrary reminder text. */
|
|
17
|
-
export function subagentBaseline(value: unknown): SubagentBaseline | undefined {
|
|
18
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
19
|
-
const input = value as Record<string, unknown>;
|
|
20
|
-
const output = emptySubagentBaseline();
|
|
21
|
-
for (const key of ["models", "agents", "delegation"] as const) {
|
|
22
|
-
if (input[key] !== null && typeof input[key] !== "string") return undefined;
|
|
23
|
-
output[key] = (input[key] as string | null | undefined) ?? null;
|
|
24
|
-
}
|
|
25
|
-
// v1 phase-2 snapshots did not carry this field; absence means no known
|
|
26
|
-
// refusal, not malformed state. Keep the list JSON-safe and deterministic.
|
|
27
|
-
if (input.refusedModels !== undefined && (!Array.isArray(input.refusedModels) || !input.refusedModels.every((model) => typeof model === "string"))) return undefined;
|
|
28
|
-
output.refusedModels = [...new Set((input.refusedModels as string[] | undefined) ?? [])];
|
|
29
|
-
return output;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A restored first-prepend block is a usable fallback for snapshots predating structured baselines. */
|
|
33
|
-
export function subagentBaselineFromStack(stack: readonly { key?: string; text: string }[]): SubagentBaseline {
|
|
34
|
-
const baseline = emptySubagentBaseline();
|
|
35
|
-
const keys: Record<SubagentCapability, string> = {
|
|
36
|
-
models: "subagent-models", agents: "subagent-agents", delegation: "subagent-delegation",
|
|
37
|
-
};
|
|
38
|
-
for (const capability of Object.keys(keys) as SubagentCapability[]) {
|
|
39
|
-
const entry = stack.find((item) => item.key === keys[capability]);
|
|
40
|
-
baseline[capability] = entry?.text ?? null;
|
|
41
|
-
}
|
|
42
|
-
return baseline;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type SubagentAnnouncement =
|
|
46
|
-
| { kind: "none"; baseline: SubagentBaseline }
|
|
47
|
-
| { kind: "standing"; baseline: SubagentBaseline }
|
|
48
|
-
| { kind: "addendum"; text: string; baseline: SubagentBaseline };
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Fresh sessions may rewrite message 1; resumed sessions must leave its locked
|
|
52
|
-
* block alone and describe a real capability change at the tail instead.
|
|
53
|
-
*/
|
|
54
|
-
export function planSubagentAnnouncement(input: {
|
|
55
|
-
restored: boolean;
|
|
56
|
-
baseline: SubagentBaseline;
|
|
57
|
-
capability: SubagentCapability;
|
|
58
|
-
text: string | null;
|
|
59
|
-
}): SubagentAnnouncement {
|
|
60
|
-
if (input.baseline[input.capability] === input.text) return { kind: "none", baseline: input.baseline };
|
|
61
|
-
const baseline = { ...input.baseline, [input.capability]: input.text };
|
|
62
|
-
if (!input.restored) return { kind: "standing", baseline };
|
|
63
|
-
if (input.text !== null) return { kind: "addendum", text: input.text, baseline };
|
|
64
|
-
return {
|
|
65
|
-
kind: "addendum",
|
|
66
|
-
text: "The subagent delegation guidance previously shown for this conversation is no longer active.",
|
|
67
|
-
baseline,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What to tell the model when the deferred-tool set changes (pure).
|
|
3
|
-
*
|
|
4
|
-
* The deferred-tools listing is a `first-prepend` reminder on message 1, so it
|
|
5
|
-
* is part of the cached prefix of every later request. Before the first request
|
|
6
|
-
* nothing is cached and the listing can be rewritten freely; after it, any
|
|
7
|
-
* rewrite re-caches the whole conversation (measured 2026-09-04: 16.7k tokens
|
|
8
|
-
* re-written on request 2 of a run whose MCP tools registered inside the
|
|
9
|
-
* announce debounce). So once a request has gone out the listing is FROZEN and
|
|
10
|
-
* later arrivals ride a one-shot addendum where the model reads next. Names that
|
|
11
|
-
* disappear are not announced: the frozen listing keeps them and `tool_search`
|
|
12
|
-
* reports "not found" if the model asks for one.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export type AnnouncementPlan =
|
|
16
|
-
| { kind: "none" }
|
|
17
|
-
/** Nothing is cached yet: (re)write the standing listing with every name. */
|
|
18
|
-
| { kind: "rewrite"; names: string[] }
|
|
19
|
-
/** Message 1 is frozen: announce only the new names as a one-shot. */
|
|
20
|
-
| { kind: "addendum"; added: string[] };
|
|
21
|
-
|
|
22
|
-
export function planAnnouncement(input: {
|
|
23
|
-
requestSent: boolean;
|
|
24
|
-
announced: ReadonlySet<string>;
|
|
25
|
-
available: readonly string[];
|
|
26
|
-
}): AnnouncementPlan {
|
|
27
|
-
if (!input.requestSent) {
|
|
28
|
-
return input.available.length === 0 ? { kind: "none" } : { kind: "rewrite", names: [...input.available] };
|
|
29
|
-
}
|
|
30
|
-
const added = input.available.filter((name) => !input.announced.has(name));
|
|
31
|
-
return added.length === 0 ? { kind: "none" } : { kind: "addendum", added };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** JSON-safe persisted baseline for the names already described to the model. */
|
|
35
|
-
export function deferredToolsBaseline(value: unknown): string[] | undefined {
|
|
36
|
-
if (!Array.isArray(value) || !value.every((name) => typeof name === "string")) return undefined;
|
|
37
|
-
return [...new Set(value)];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Compatibility fallback for a v1 stack written before structured baselines. */
|
|
41
|
-
export function deferredToolsFromReminder(text: string | undefined): string[] | undefined {
|
|
42
|
-
if (!text) return undefined;
|
|
43
|
-
const marker = "\n";
|
|
44
|
-
const lines = text.split(marker);
|
|
45
|
-
return lines.length > 1 && lines[0].startsWith("The following deferred tools are available via tool_search")
|
|
46
|
-
? lines.slice(1).filter(Boolean)
|
|
47
|
-
: undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Bring the announced set in line with what the plan told the model. */
|
|
51
|
-
export function applyAnnouncement(announced: Set<string>, plan: AnnouncementPlan): void {
|
|
52
|
-
if (plan.kind === "rewrite") {
|
|
53
|
-
announced.clear();
|
|
54
|
-
for (const name of plan.names) announced.add(name);
|
|
55
|
-
} else if (plan.kind === "addendum") {
|
|
56
|
-
for (const name of plan.added) announced.add(name);
|
|
57
|
-
}
|
|
58
|
-
}
|