@code-yeongyu/senpi 2026.9.8 → 2026.9.9
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 +23 -4
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +2 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/compaction/compaction-execution.d.ts.map +1 -1
- package/dist/core/compaction/compaction-execution.js +5 -5
- package/dist/core/compaction/compaction-execution.js.map +1 -1
- package/dist/core/compaction/compaction-settings.d.ts +2 -0
- package/dist/core/compaction/compaction-settings.d.ts.map +1 -1
- package/dist/core/compaction/compaction-settings.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -7
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/stream-watchdog.d.ts +29 -0
- package/dist/core/compaction/stream-watchdog.d.ts.map +1 -1
- package/dist/core/compaction/stream-watchdog.js +35 -0
- package/dist/core/compaction/stream-watchdog.js.map +1 -1
- package/dist/core/compaction-settings-access.d.ts +6 -0
- package/dist/core/compaction-settings-access.d.ts.map +1 -1
- package/dist/core/compaction-settings-access.js.map +1 -1
- package/dist/core/compaction-settings-resolver.d.ts +2 -0
- package/dist/core/compaction-settings-resolver.d.ts.map +1 -1
- package/dist/core/compaction-settings-resolver.js +5 -0
- package/dist/core/compaction-settings-resolver.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts +1 -1
- package/dist/core/extensions/builtin/compaction/model-usability-budget.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/model-usability-budget.js +23 -3
- package/dist/core/extensions/builtin/compaction/model-usability-budget.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/speculative-summary.d.ts +2 -0
- package/dist/core/extensions/builtin/compaction/speculative-summary.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/speculative-summary.js +5 -2
- package/dist/core/extensions/builtin/compaction/speculative-summary.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/speculative.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/speculative.js +9 -3
- package/dist/core/extensions/builtin/compaction/speculative.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/summarization-retry.d.ts +10 -4
- package/dist/core/extensions/builtin/compaction/summarization-retry.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/summarization-retry.js +14 -5
- package/dist/core/extensions/builtin/compaction/summarization-retry.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +11 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-write-reservation.d.ts +4 -0
- package/dist/core/session-write-reservation.d.ts.map +1 -0
- package/dist/core/session-write-reservation.js +12 -0
- package/dist/core/session-write-reservation.js.map +1 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +162 -147
- package/dist/main.js.map +1 -1
- package/dist/modes/rpc/connection-handler.d.ts +2 -0
- package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
- package/dist/modes/rpc/connection-handler.js +1 -1
- package/dist/modes/rpc/connection-handler.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.d.ts +2 -0
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +18 -8
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/session-binding.d.ts.map +1 -1
- package/dist/modes/rpc/session-binding.js +3 -0
- package/dist/modes/rpc/session-binding.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts +1 -1
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +57 -25
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-event-writer.d.ts +23 -1
- package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
- package/dist/modes/rpc/session-event-writer.js +108 -1
- package/dist/modes/rpc/session-event-writer.js.map +1 -1
- package/dist/modes/rpc/session-registry.d.ts +4 -2
- package/dist/modes/rpc/session-registry.d.ts.map +1 -1
- package/dist/modes/rpc/session-registry.js +1 -1
- package/dist/modes/rpc/session-registry.js.map +1 -1
- package/dist/modes/rpc/session-worker-client.d.ts +47 -0
- package/dist/modes/rpc/session-worker-client.d.ts.map +1 -0
- package/dist/modes/rpc/session-worker-client.js +226 -0
- package/dist/modes/rpc/session-worker-client.js.map +1 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts +103 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts.map +1 -0
- package/dist/modes/rpc/session-worker-protocol.js +12 -0
- package/dist/modes/rpc/session-worker-protocol.js.map +1 -0
- package/dist/modes/rpc/session-worker-requests.d.ts +24 -0
- package/dist/modes/rpc/session-worker-requests.d.ts.map +1 -0
- package/dist/modes/rpc/session-worker-requests.js +71 -0
- package/dist/modes/rpc/session-worker-requests.js.map +1 -0
- package/dist/modes/rpc/session-worker.d.ts +2 -0
- package/dist/modes/rpc/session-worker.d.ts.map +1 -0
- package/dist/modes/rpc/session-worker.js +229 -0
- package/dist/modes/rpc/session-worker.js.map +1 -0
- package/dist/modes/rpc/worker-session-registry.d.ts +37 -0
- package/dist/modes/rpc/worker-session-registry.d.ts.map +1 -0
- package/dist/modes/rpc/worker-session-registry.js +207 -0
- package/dist/modes/rpc/worker-session-registry.js.map +1 -0
- package/docs/rpc.md +70 -9
- package/docs/settings.md +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +23 -5
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +30 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +30 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { ProviderScope } from "@earendil-works/pi-ai/node/provider-scope";
|
|
3
|
+
import { RpcSessionRegistryError, } from "./session-registry.js";
|
|
4
|
+
import { SessionWorkerClient } from "./session-worker-client.js";
|
|
5
|
+
import { SESSION_WORKER_LIMITS } from "./session-worker-protocol.js";
|
|
6
|
+
/** Transport-side lifecycle owner. Caller paths are never inspected on this event loop. */
|
|
7
|
+
export class WorkerSessionRegistry {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.entries = new Map();
|
|
10
|
+
this.reservations = new Map();
|
|
11
|
+
this.serial = 0;
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.closeGraceMs = options.closeGraceMs;
|
|
14
|
+
this.now = options.now;
|
|
15
|
+
}
|
|
16
|
+
get size() {
|
|
17
|
+
return this.entries.size;
|
|
18
|
+
}
|
|
19
|
+
async openSession(profile) {
|
|
20
|
+
if (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath)))
|
|
21
|
+
throw new RpcSessionRegistryError("invalid_path");
|
|
22
|
+
if (profile.sessionPath) {
|
|
23
|
+
const key = this.knownReservationKey(profile.sessionPath);
|
|
24
|
+
const owner = key ? this.reservations.get(key) : undefined;
|
|
25
|
+
if (key && owner)
|
|
26
|
+
return this.attach(owner, key);
|
|
27
|
+
}
|
|
28
|
+
if (this.size >= SESSION_WORKER_LIMITS.workers)
|
|
29
|
+
throw new Error("too_many_sessions");
|
|
30
|
+
const handle = `rpc-${++this.serial}`;
|
|
31
|
+
const entry = {
|
|
32
|
+
state: "opening",
|
|
33
|
+
scope: new ProviderScope(),
|
|
34
|
+
profile: Object.freeze({ ...profile }),
|
|
35
|
+
cwd: profile.cwd,
|
|
36
|
+
attachments: 1,
|
|
37
|
+
lastCommandAt: this.now(),
|
|
38
|
+
lifecycleMutex: Promise.resolve(),
|
|
39
|
+
};
|
|
40
|
+
const worker = new SessionWorkerClient({
|
|
41
|
+
reserve: (path) => this.reserve(handle, path),
|
|
42
|
+
exit: () => {
|
|
43
|
+
if (this.entries.get(handle) !== entry)
|
|
44
|
+
return;
|
|
45
|
+
entry.state = "closed";
|
|
46
|
+
this.entries.delete(handle);
|
|
47
|
+
for (const [path, owner] of this.reservations)
|
|
48
|
+
if (owner === handle)
|
|
49
|
+
this.reservations.delete(path);
|
|
50
|
+
entry.closeResolve?.();
|
|
51
|
+
},
|
|
52
|
+
failure: (error) => {
|
|
53
|
+
entry.state = "quarantined";
|
|
54
|
+
process.stderr.write(`senpi rpc session ${handle} quarantined: ${error}\n`);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
entry.worker = worker;
|
|
58
|
+
this.entries.set(handle, entry);
|
|
59
|
+
try {
|
|
60
|
+
const path = await worker.prepare(this.options.configuration, profile);
|
|
61
|
+
const owner = this.reservations.get(path);
|
|
62
|
+
if (owner) {
|
|
63
|
+
const attached = this.attach(owner, path);
|
|
64
|
+
entry.state = "quarantined";
|
|
65
|
+
worker.quarantine();
|
|
66
|
+
return attached;
|
|
67
|
+
}
|
|
68
|
+
if (!this.reserve(handle, path))
|
|
69
|
+
throw new RpcSessionRegistryError("session_path_in_use");
|
|
70
|
+
entry.reservationKey = path;
|
|
71
|
+
entry.sessionPath = path;
|
|
72
|
+
const snapshot = await worker.commit();
|
|
73
|
+
if (entry.state !== "opening")
|
|
74
|
+
throw new RpcSessionRegistryError("session_closing");
|
|
75
|
+
entry.durableSessionId = snapshot.state.sessionId;
|
|
76
|
+
entry.cwd = snapshot.state.cwd;
|
|
77
|
+
entry.state = "open";
|
|
78
|
+
return this.openResult(handle, entry);
|
|
79
|
+
}
|
|
80
|
+
catch (cause) {
|
|
81
|
+
entry.state = "quarantined";
|
|
82
|
+
worker.quarantine();
|
|
83
|
+
throw cause;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
peek(handle) {
|
|
87
|
+
return this.entries.get(handle);
|
|
88
|
+
}
|
|
89
|
+
getForCommand(handle, command) {
|
|
90
|
+
const entry = this.entries.get(handle);
|
|
91
|
+
if (!entry)
|
|
92
|
+
throw new RpcSessionRegistryError("unknown_session");
|
|
93
|
+
if (entry.state === "quarantined" ||
|
|
94
|
+
(entry.state === "closing" && !["abort", "abort_bash", "extension_ui_response"].includes(command)))
|
|
95
|
+
throw new RpcSessionRegistryError("session_closing");
|
|
96
|
+
if (entry.state !== "open" && entry.state !== "closing")
|
|
97
|
+
throw new RpcSessionRegistryError("unknown_session");
|
|
98
|
+
entry.lastCommandAt = this.now();
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
beginClose(handle, onRole) {
|
|
102
|
+
const entry = this.entries.get(handle);
|
|
103
|
+
if (!entry)
|
|
104
|
+
throw new RpcSessionRegistryError("unknown_session");
|
|
105
|
+
if (entry.state === "closing" || entry.state === "quarantined") {
|
|
106
|
+
onRole?.(false);
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
if (entry.state !== "open" && entry.state !== "opening")
|
|
110
|
+
throw new RpcSessionRegistryError("unknown_session");
|
|
111
|
+
entry.attachments--;
|
|
112
|
+
if (entry.attachments > 0)
|
|
113
|
+
return entry;
|
|
114
|
+
entry.state = "closing";
|
|
115
|
+
entry.closeCompletion = new Promise((resolve) => {
|
|
116
|
+
entry.closeResolve = resolve;
|
|
117
|
+
});
|
|
118
|
+
onRole?.(true);
|
|
119
|
+
return entry;
|
|
120
|
+
}
|
|
121
|
+
close(handle) {
|
|
122
|
+
const entry = this.beginClose(handle);
|
|
123
|
+
return entry.state === "closing" ? this.closeMarked(handle) : Promise.resolve();
|
|
124
|
+
}
|
|
125
|
+
async closeMarked(handle) {
|
|
126
|
+
const entry = this.entries.get(handle);
|
|
127
|
+
if (entry?.state !== "closing" || !entry.worker)
|
|
128
|
+
throw new RpcSessionRegistryError("unknown_session");
|
|
129
|
+
if (entry.closeStarted)
|
|
130
|
+
return entry.closeCompletion;
|
|
131
|
+
entry.closeStarted = true;
|
|
132
|
+
// Reply on a bounded deadline, but keep entry, attachments and reservations until exit.
|
|
133
|
+
let timer;
|
|
134
|
+
await Promise.race([
|
|
135
|
+
entry.worker.close(this.closeGraceMs),
|
|
136
|
+
new Promise((resolve) => {
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
if (entry.state !== "closed")
|
|
139
|
+
entry.state = "quarantined";
|
|
140
|
+
resolve();
|
|
141
|
+
}, this.closeGraceMs);
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
144
|
+
if (timer)
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
list() {
|
|
148
|
+
return [...this.entries].map(([sessionId, entry]) => {
|
|
149
|
+
const state = entry.worker?.snapshot?.state;
|
|
150
|
+
return {
|
|
151
|
+
sessionId,
|
|
152
|
+
durableSessionId: state?.sessionId ?? entry.durableSessionId,
|
|
153
|
+
sessionPath: state?.sessionFile ?? entry.sessionPath,
|
|
154
|
+
cwd: state?.cwd ?? entry.cwd,
|
|
155
|
+
name: state?.sessionName,
|
|
156
|
+
status: entry.state === "quarantined" ? "closing" : entry.state,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Only compare spellings already tied to a granted identity; do not inspect caller paths here. */
|
|
161
|
+
knownReservationKey(path) {
|
|
162
|
+
if (this.reservations.has(path))
|
|
163
|
+
return path;
|
|
164
|
+
for (const entry of this.entries.values()) {
|
|
165
|
+
if (entry.profile.sessionPath === path && entry.reservationKey)
|
|
166
|
+
return entry.reservationKey;
|
|
167
|
+
const snapshot = entry.worker?.snapshot;
|
|
168
|
+
if (snapshot?.state.sessionFile === path)
|
|
169
|
+
return snapshot.sessionPath;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
attach(owner, path) {
|
|
174
|
+
const entry = this.entries.get(owner);
|
|
175
|
+
if (entry?.state !== "open" || !entry.worker?.bindingReady || entry.worker.snapshot?.sessionPath !== path)
|
|
176
|
+
throw new RpcSessionRegistryError("session_path_in_use");
|
|
177
|
+
const result = this.openResult(owner, entry);
|
|
178
|
+
entry.attachments++;
|
|
179
|
+
entry.lastCommandAt = this.now();
|
|
180
|
+
return { ...result, attached: true };
|
|
181
|
+
}
|
|
182
|
+
reserve(handle, path) {
|
|
183
|
+
const entry = this.entries.get(handle);
|
|
184
|
+
if (!entry)
|
|
185
|
+
return false;
|
|
186
|
+
const owner = this.reservations.get(path);
|
|
187
|
+
if (owner)
|
|
188
|
+
return owner === handle;
|
|
189
|
+
if (entry.state !== "opening" && entry.state !== "open")
|
|
190
|
+
return false;
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (const current of this.reservations.values())
|
|
193
|
+
if (current === handle)
|
|
194
|
+
count++;
|
|
195
|
+
if (count >= SESSION_WORKER_LIMITS.reservations)
|
|
196
|
+
return false;
|
|
197
|
+
this.reservations.set(path, handle);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
openResult(handle, entry) {
|
|
201
|
+
const state = entry.worker?.snapshot?.state;
|
|
202
|
+
if (!state)
|
|
203
|
+
throw new RpcSessionRegistryError("open_failed");
|
|
204
|
+
return { sessionId: handle, durableSessionId: state.sessionId, sessionPath: state.sessionFile };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=worker-session-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-session-registry.js","sourceRoot":"","sources":["../../../src/modes/rpc/worker-session-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute } from \"node:path\";\nimport { ProviderScope } from \"@earendil-works/pi-ai/node/provider-scope\";\nimport type { CliRuntimeConfiguration } from \"../../main.ts\";\nimport {\n\ttype OpenRpcSession,\n\ttype RpcSessionEntry,\n\ttype RpcSessionLaunchProfile,\n\tRpcSessionRegistryError,\n} from \"./session-registry.ts\";\nimport { SessionWorkerClient } from \"./session-worker-client.ts\";\nimport { SESSION_WORKER_LIMITS } from \"./session-worker-protocol.ts\";\n\n/** Transport-side lifecycle owner. Caller paths are never inspected on this event loop. */\nexport class WorkerSessionRegistry {\n\tprivate readonly entries = new Map<string, RpcSessionEntry>();\n\tprivate readonly reservations = new Map<string, string>();\n\tprivate serial = 0;\n\treadonly closeGraceMs: number;\n\tprivate readonly now: () => number;\n\n\tprivate readonly options: { configuration: CliRuntimeConfiguration; closeGraceMs: number; now: () => number };\n\n\tconstructor(options: { configuration: CliRuntimeConfiguration; closeGraceMs: number; now: () => number }) {\n\t\tthis.options = options;\n\t\tthis.closeGraceMs = options.closeGraceMs;\n\t\tthis.now = options.now;\n\t}\n\n\tget size(): number {\n\t\treturn this.entries.size;\n\t}\n\n\tasync openSession(profile: RpcSessionLaunchProfile): Promise<OpenRpcSession> {\n\t\tif (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath)))\n\t\t\tthrow new RpcSessionRegistryError(\"invalid_path\");\n\t\tif (profile.sessionPath) {\n\t\t\tconst key = this.knownReservationKey(profile.sessionPath);\n\t\t\tconst owner = key ? this.reservations.get(key) : undefined;\n\t\t\tif (key && owner) return this.attach(owner, key);\n\t\t}\n\t\tif (this.size >= SESSION_WORKER_LIMITS.workers) throw new Error(\"too_many_sessions\");\n\t\tconst handle = `rpc-${++this.serial}`;\n\t\tconst entry: RpcSessionEntry = {\n\t\t\tstate: \"opening\",\n\t\t\tscope: new ProviderScope(),\n\t\t\tprofile: Object.freeze({ ...profile }),\n\t\t\tcwd: profile.cwd,\n\t\t\tattachments: 1,\n\t\t\tlastCommandAt: this.now(),\n\t\t\tlifecycleMutex: Promise.resolve(),\n\t\t};\n\t\tconst worker = new SessionWorkerClient({\n\t\t\treserve: (path) => this.reserve(handle, path),\n\t\t\texit: () => {\n\t\t\t\tif (this.entries.get(handle) !== entry) return;\n\t\t\t\tentry.state = \"closed\";\n\t\t\t\tthis.entries.delete(handle);\n\t\t\t\tfor (const [path, owner] of this.reservations) if (owner === handle) this.reservations.delete(path);\n\t\t\t\tentry.closeResolve?.();\n\t\t\t},\n\t\t\tfailure: (error) => {\n\t\t\t\tentry.state = \"quarantined\";\n\t\t\t\tprocess.stderr.write(`senpi rpc session ${handle} quarantined: ${error}\\n`);\n\t\t\t},\n\t\t});\n\t\tentry.worker = worker;\n\t\tthis.entries.set(handle, entry);\n\t\ttry {\n\t\t\tconst path = await worker.prepare(this.options.configuration, profile);\n\t\t\tconst owner = this.reservations.get(path);\n\t\t\tif (owner) {\n\t\t\t\tconst attached = this.attach(owner, path);\n\t\t\t\tentry.state = \"quarantined\";\n\t\t\t\tworker.quarantine();\n\t\t\t\treturn attached;\n\t\t\t}\n\t\t\tif (!this.reserve(handle, path)) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\tentry.reservationKey = path;\n\t\t\tentry.sessionPath = path;\n\t\t\tconst snapshot = await worker.commit();\n\t\t\tif (entry.state !== \"opening\") throw new RpcSessionRegistryError(\"session_closing\");\n\t\t\tentry.durableSessionId = snapshot.state.sessionId;\n\t\t\tentry.cwd = snapshot.state.cwd;\n\t\t\tentry.state = \"open\";\n\t\t\treturn this.openResult(handle, entry);\n\t\t} catch (cause) {\n\t\t\tentry.state = \"quarantined\";\n\t\t\tworker.quarantine();\n\t\t\tthrow cause;\n\t\t}\n\t}\n\n\tpeek(handle: string): RpcSessionEntry | undefined {\n\t\treturn this.entries.get(handle);\n\t}\n\n\tgetForCommand(handle: string, command: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (\n\t\t\tentry.state === \"quarantined\" ||\n\t\t\t(entry.state === \"closing\" && ![\"abort\", \"abort_bash\", \"extension_ui_response\"].includes(command))\n\t\t)\n\t\t\tthrow new RpcSessionRegistryError(\"session_closing\");\n\t\tif (entry.state !== \"open\" && entry.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.lastCommandAt = this.now();\n\t\treturn entry;\n\t}\n\n\tbeginClose(handle: string, onRole?: (finalizer: boolean) => void): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.state === \"closing\" || entry.state === \"quarantined\") {\n\t\t\tonRole?.(false);\n\t\t\treturn entry;\n\t\t}\n\t\tif (entry.state !== \"open\" && entry.state !== \"opening\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.attachments--;\n\t\tif (entry.attachments > 0) return entry;\n\t\tentry.state = \"closing\";\n\t\tentry.closeCompletion = new Promise((resolve) => {\n\t\t\tentry.closeResolve = resolve;\n\t\t});\n\t\tonRole?.(true);\n\t\treturn entry;\n\t}\n\n\tclose(handle: string): Promise<void> {\n\t\tconst entry = this.beginClose(handle);\n\t\treturn entry.state === \"closing\" ? this.closeMarked(handle) : Promise.resolve();\n\t}\n\n\tasync closeMarked(handle: string): Promise<void> {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"closing\" || !entry.worker) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.closeStarted) return entry.closeCompletion;\n\t\tentry.closeStarted = true;\n\t\t// Reply on a bounded deadline, but keep entry, attachments and reservations until exit.\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\tawait Promise.race([\n\t\t\tentry.worker.close(this.closeGraceMs),\n\t\t\tnew Promise<void>((resolve) => {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tif (entry.state !== \"closed\") entry.state = \"quarantined\";\n\t\t\t\t\tresolve();\n\t\t\t\t}, this.closeGraceMs);\n\t\t\t}),\n\t\t]);\n\t\tif (timer) clearTimeout(timer);\n\t}\n\n\tlist(): Array<{\n\t\tsessionId: string;\n\t\tdurableSessionId?: string;\n\t\tsessionPath?: string;\n\t\tcwd: string;\n\t\tname?: string;\n\t\tstatus: Exclude<RpcSessionEntry[\"state\"], \"quarantined\">;\n\t}> {\n\t\treturn [...this.entries].map(([sessionId, entry]) => {\n\t\t\tconst state = entry.worker?.snapshot?.state;\n\t\t\treturn {\n\t\t\t\tsessionId,\n\t\t\t\tdurableSessionId: state?.sessionId ?? entry.durableSessionId,\n\t\t\t\tsessionPath: state?.sessionFile ?? entry.sessionPath,\n\t\t\t\tcwd: state?.cwd ?? entry.cwd,\n\t\t\t\tname: state?.sessionName,\n\t\t\t\tstatus: entry.state === \"quarantined\" ? \"closing\" : entry.state,\n\t\t\t};\n\t\t});\n\t}\n\n\t/** Only compare spellings already tied to a granted identity; do not inspect caller paths here. */\n\tprivate knownReservationKey(path: string): string | undefined {\n\t\tif (this.reservations.has(path)) return path;\n\t\tfor (const entry of this.entries.values()) {\n\t\t\tif (entry.profile.sessionPath === path && entry.reservationKey) return entry.reservationKey;\n\t\t\tconst snapshot = entry.worker?.snapshot;\n\t\t\tif (snapshot?.state.sessionFile === path) return snapshot.sessionPath;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate attach(owner: string, path: string): OpenRpcSession {\n\t\tconst entry = this.entries.get(owner);\n\t\tif (entry?.state !== \"open\" || !entry.worker?.bindingReady || entry.worker.snapshot?.sessionPath !== path)\n\t\t\tthrow new RpcSessionRegistryError(\"session_path_in_use\");\n\t\tconst result = this.openResult(owner, entry);\n\t\tentry.attachments++;\n\t\tentry.lastCommandAt = this.now();\n\t\treturn { ...result, attached: true };\n\t}\n\n\tprivate reserve(handle: string, path: string): boolean {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) return false;\n\t\tconst owner = this.reservations.get(path);\n\t\tif (owner) return owner === handle;\n\t\tif (entry.state !== \"opening\" && entry.state !== \"open\") return false;\n\t\tlet count = 0;\n\t\tfor (const current of this.reservations.values()) if (current === handle) count++;\n\t\tif (count >= SESSION_WORKER_LIMITS.reservations) return false;\n\t\tthis.reservations.set(path, handle);\n\t\treturn true;\n\t}\n\n\tprivate openResult(handle: string, entry: RpcSessionEntry): OpenRpcSession {\n\t\tconst state = entry.worker?.snapshot?.state;\n\t\tif (!state) throw new RpcSessionRegistryError(\"open_failed\");\n\t\treturn { sessionId: handle, durableSessionId: state.sessionId, sessionPath: state.sessionFile };\n\t}\n}\n"]}
|
package/docs/rpc.md
CHANGED
|
@@ -177,8 +177,8 @@ hosts) sees neither variable and is unaffected. A host whose supervisor is alive
|
|
|
177
177
|
|
|
178
178
|
### Shared host occupancy (idle eviction, session cap, empty-host exit)
|
|
179
179
|
|
|
180
|
-
Every
|
|
181
|
-
|
|
180
|
+
Every CLI shared-host session owns a worker isolate and a complete runtime. Memory depends on its extensions and
|
|
181
|
+
session contents; isolates do not provide process-fatal OOM containment. The host enforces these occupancy bounds:
|
|
182
182
|
|
|
183
183
|
- **Idle eviction**: a session with no routed command and no session-owned work for
|
|
184
184
|
`SENPI_RPC_SESSION_IDLE_EVICTION_MS` (default 30 minutes) is closed through the exact `close_session` sequence
|
|
@@ -188,9 +188,13 @@ occupancy bounds itself, independent of the supervisor and of client cooperation
|
|
|
188
188
|
background terminal jobs and any other published wake source (terminal monitors, loop-guard holds), compaction,
|
|
189
189
|
and barrier-held session work all defer eviction, and the idle clock restarts when that work settles. An evicted
|
|
190
190
|
session resumes like any other: the next `open_session` with the same `sessionPath` reopens it.
|
|
191
|
-
- **
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
- **Worker capacity**: at most 20 workers may be preparing, open, closing, or quarantined together. Admission beyond
|
|
192
|
+
this bound fails explicitly with `open_failed: too_many_sessions`; it never evicts another session or starts an OS
|
|
193
|
+
process as a fallback. This is a new externally visible bound for CLI shared mode, which previously admitted
|
|
194
|
+
unlimited logical sessions. It applies to new worker allocation, not attachments: a known canonical path or
|
|
195
|
+
original opening spelling joins its already-bound owner without allocating a worker, even at capacity. An
|
|
196
|
+
unknown path/alias still needs a preparation worker slot. In-process SDK registries using an injected runtime
|
|
197
|
+
factory retain their existing behavior.
|
|
194
198
|
- **Empty-host exit**: when the registry holds zero sessions AND no client is connected, continuously for
|
|
195
199
|
`SENPI_RPC_HOST_EMPTY_EXIT_MS` (default 15 minutes), the host exits through its clean shutdown path (flush, socket
|
|
196
200
|
removal), for stdio and `--listen` hosts alike. A connected client counts as occupancy even with no session open,
|
|
@@ -200,14 +204,71 @@ occupancy bounds itself, independent of the supervisor and of client cooperation
|
|
|
200
204
|
Values are positive integers; invalid values fall through to the defaults. These lifecycle windows run inside the host process,
|
|
201
205
|
so they hold even for embedders and hand-started hosts that have no supervisor.
|
|
202
206
|
|
|
207
|
+
### Worker ownership and flow control
|
|
208
|
+
|
|
209
|
+
The main host owns transport, attachments and canonical reservations. Workers canonicalize caller-supplied paths;
|
|
210
|
+
main does not synchronously traverse those paths. An open prepares a path, obtains the main host's exclusive grant,
|
|
211
|
+
and only then constructs its session writer and runtime. A conflicting alias attaches to an open owner or fails
|
|
212
|
+
explicitly before opening another writer.
|
|
213
|
+
|
|
214
|
+
SessionManager writes, switches, forks, new sessions and imports obtain the same grant before writer creation or
|
|
215
|
+
append-side normalization. Acquired paths are conservatively retained for that worker's entire lifetime, including
|
|
216
|
+
superseded paths after a switch. Each worker may reserve at most 64 paths; an exhausted reservation budget fails
|
|
217
|
+
explicitly. Close or an opening deadline requests worker termination, but does not release reservations or worker
|
|
218
|
+
capacity until the actual exit event. A syscall that cannot yet be interrupted can therefore keep an entry
|
|
219
|
+
internally quarantined after the routing handle has closed. `list_sessions` continues to publish `closing`, not a
|
|
220
|
+
new status: existing clients must not mistake a quarantined worker for a live reattach target. Retry the path only
|
|
221
|
+
after that entry disappears.
|
|
222
|
+
|
|
223
|
+
Each worker accepts at most 64 ordinary pending requests totaling 16 MiB, plus four reserved interrupt/UI-response
|
|
224
|
+
slots totaling 1 MiB. Prepare, commit and bind share one 30-second opening budget starting with worker allocation;
|
|
225
|
+
they do not each reset that budget. Interrupt requests have a five-second deadline. Ordinary commands remain bounded
|
|
226
|
+
by count and bytes without imposing a new timeout on long-running commands. The desktop's 60-second open/readmission
|
|
227
|
+
window is a separate client-side wait: the host normally reports its earlier 30-second failure within that window,
|
|
228
|
+
but a slow transport can delay delivery. Neither timeout proves worker exit or permits concurrent reopening.
|
|
229
|
+
Display updates coalesce to one pending update and one latest value; UI cancellation and close have separate control
|
|
230
|
+
messages. IPC output and snapshots are limited to 16 MiB per record, with one acknowledged record at a time. Credit
|
|
231
|
+
returns after the session's destinations consume their output, not merely on IPC receipt. A five-second credit
|
|
232
|
+
failure closes that session visibly (`session_error` followed by `session_closed`), rather than retaining an
|
|
233
|
+
unbounded queue. Socket queues retain their existing independent overflow/disconnect behavior. The default stdio
|
|
234
|
+
queue is bounded at 64 MiB or 4096 records, with reserved terminal-failure records and one control-overflow notice.
|
|
235
|
+
Close admission counts both queued output and pending close replies (including their serialized bytes) before
|
|
236
|
+
releasing an attachment or waiting for teardown. An admitted first closer reserves its lifecycle and terminal reply;
|
|
237
|
+
admitted joiners follow those records in FIFO order. Excess closes are not admitted and do not release ownership.
|
|
238
|
+
One bounded `overflow` record with `command: "close_session"` and
|
|
239
|
+
`error: "rpc_close_output_overflow, resync required"` reports the saturation episode instead of retaining a reply
|
|
240
|
+
or promise for each rejected request. Over sockets, this notice goes only to the requester whose close was rejected;
|
|
241
|
+
each connection may have at most one outstanding notice, released when its sink consumes it. After socket-only drain,
|
|
242
|
+
a subsequent saturation episode on the same connection can report a new notice without reconnecting. A healthy peer is not
|
|
243
|
+
told to resynchronize, and a newly affected or reconnected peer receives its own notice. Stdio retains one notice per
|
|
244
|
+
episode. Clients receiving a notice must stop issuing closes, drain output, and resynchronize unacknowledged requests; the notice is not a successful close acknowledgment. Admission resumes as capacity becomes available.
|
|
245
|
+
Canonical reservations and worker capacity remain held until native exit, including after close-output overflow.
|
|
246
|
+
|
|
247
|
+
The classic handler, extension UI bridge, renderer callbacks and provider scope run inside the owning worker; only
|
|
248
|
+
plain data crosses IPC. Inline `main()` extension factories cannot be cloned and are rejected in shared mode: use
|
|
249
|
+
file-backed extensions. Classic single-session RPC remains in-process. Standalone Bun builds must embed
|
|
250
|
+
`src/modes/rpc/session-worker.ts` as an explicit entrypoint; Node bundles must ship `session-worker.js` beside the
|
|
251
|
+
chunk containing its worker client. Third-party/rebranded Bun wrappers must pass the published
|
|
252
|
+
`dist/modes/rpc/session-worker.js` as an additional compile entry, set an explicit `--root`, and set
|
|
253
|
+
`--define=SENPI_RPC_SESSION_WORKER_ENTRY='"./<worker-path-relative-to-root>"'`. The define is a build-time
|
|
254
|
+
contract, not an environment variable. Its path must match Bun's embedded entry name, not the source machine's
|
|
255
|
+
absolute path or the runtime working directory. The client resolves it against its compiled `import.meta.url`
|
|
256
|
+
before constructing the Worker: Bun 1.4.0 resolves a bare relative Worker string against the real cwd instead
|
|
257
|
+
of the embedded filesystem. Verify the relocated wrapper on the wrapper's supported Bun version and platform
|
|
258
|
+
by opening two shared sessions;
|
|
259
|
+
a standalone Senpi smoke does not verify a wrapper's different compile graph.
|
|
260
|
+
|
|
261
|
+
Workers isolate JavaScript event loops, not OS processes: they do not promise syscall cancellation, process-fatal OOM
|
|
262
|
+
containment, or containment of arbitrary native code. They are not an extension sandbox.
|
|
263
|
+
|
|
203
264
|
### D1 normative table (multi-session mode)
|
|
204
265
|
|
|
205
266
|
| Command | Params | Success data | Notes |
|
|
206
267
|
| --- | --- | --- | --- |
|
|
207
268
|
| `get_protocol_info` | - | `{ protocolVersion: 1, serverVersion: string, capabilities: string[], mode: "classic"\|"multi" }` | Answered in BOTH modes; side-effect-free; the capability probe. Multi-session hosts include `multi_session` plus the negotiated launch capabilities. |
|
|
208
269
|
| `open_session` | `sessionPath?`, `cwd?`, `provider?`, `modelId?`, `thinkingLevel?`, `permissionPreset?` (all optional; paths MUST be absolute) | `{ sessionId, state: RpcSessionState, attached?: true }` | `sessionPath` = today's `--session` semantics (open-if-exists else create persisting there, `session-manager.ts:926-940`); `provider`/`modelId` applied only on create (resume restores the session's model — mirrors `SenpiSessionRuntime.ts:198-200`); params form the immutable launch profile (D8). When the path is already held by a fully-open session, the open ATTACHES to it: same routing handle, `attached: true`, one more attachment counted; the runtime is torn down only when the last attachment closes. Idle sessions past the eviction window are closed by the host itself. |
|
|
209
|
-
| `close_session` | `sessionId` | `{}` | Aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then
|
|
210
|
-
| `list_sessions` | - | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status }] }` | Includes `opening`/`closing` entries
|
|
270
|
+
| `close_session` | `sessionId` | `{}` | Aborts active work, awaits agent idle + settled persistence for up to the host grace window (default 10s), then quarantines any worker that has not exited without releasing its path reservation; its response is the LAST record tagged with that handle for the first closer — no events after (test-pinned). An admitted concurrent close joins the same teardown and receives its own successful response; output saturation rejects admission with the bounded close-overflow/resync notice described above. |
|
|
271
|
+
| `list_sessions` | - | `{ sessions: [{ sessionId, durableSessionId, sessionPath, cwd, name, status }] }` | Includes `opening`/`closing` entries. Internally quarantined workers remain externally `closing` until exit. |
|
|
211
272
|
| every existing command | + `sessionId` (REQUIRED in multi mode) | unchanged | Routed to that session. |
|
|
212
273
|
|
|
213
274
|
### Identities (D6)
|
|
@@ -220,7 +281,7 @@ In the response `error` field, machine-matchable:
|
|
|
220
281
|
|
|
221
282
|
- `unknown_session`
|
|
222
283
|
- `session_closing`
|
|
223
|
-
- `session_path_in_use` (path held by
|
|
284
|
+
- `session_path_in_use` (path held by an opening, closing, quarantined, or superseded owner; a fully-open current owner is attached instead)
|
|
224
285
|
- `missing_session_id` (session-scoped command without `sessionId` in multi mode)
|
|
225
286
|
- `multi_session_disabled` (`open_session` in classic mode)
|
|
226
287
|
- `invalid_path` (relative `sessionPath`/`cwd`)
|
|
@@ -237,7 +298,7 @@ Strict FIFO per session; one total stdout order; cross-session order unspecified
|
|
|
237
298
|
|
|
238
299
|
### Duplicate/idempotency
|
|
239
300
|
|
|
240
|
-
Duplicate `open_session` while a path reservation is held by a fully-open session → ATTACH (`attached: true`, same handle); while held by an `opening`/`closing` entry → `session_path_in_use`. `close_session` releases one attachment; the runtime is disposed only when the last attachment closes. A close for an entry already `closing` joins its in-flight teardown. `close_session` on unknown/already-closed → `unknown_session` error. The grace window is configurable by the host through `SENPI_RPC_CLOSE_GRACE_MS`. Request `id`s are client-owned; the server echoes them without dedup.
|
|
301
|
+
Duplicate `open_session` while a path reservation is held by a fully-open session → ATTACH (`attached: true`, same handle); while held by an `opening`/`closing` entry (including internal quarantine) → `session_path_in_use`. `close_session` releases one attachment; the runtime is disposed only when the last attachment closes. A close for an entry already `closing` joins its in-flight teardown. `close_session` on unknown/already-closed → `unknown_session` error. The grace window is configurable by the host through `SENPI_RPC_CLOSE_GRACE_MS`. Request `id`s are client-owned; the server echoes them without dedup.
|
|
241
302
|
|
|
242
303
|
## Protocol Overview
|
|
243
304
|
|
package/docs/settings.md
CHANGED
|
@@ -191,6 +191,7 @@ Set `PI_SKIP_VERSION_CHECK=1` to disable the senpi version update check. Use `--
|
|
|
191
191
|
| `compaction.enabled` | boolean | `true` | Enable auto-compaction |
|
|
192
192
|
| `compaction.reserveTokens` | number | `16384` | Tokens reserved for LLM response |
|
|
193
193
|
| `compaction.keepRecentTokens` | number | `20000` | Recent tokens to keep (not summarized) |
|
|
194
|
+
| `compaction.summarizationMaxDurationMs` | number | adaptive | Wall-clock budget for one summarization attempt: larger of 120s and 2ms per estimated input token, capped at 30min. Set a positive value to override |
|
|
194
195
|
|
|
195
196
|
```json
|
|
196
197
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.9",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
6
6
|
"type": "module",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@babel/parser": "8.0.4",
|
|
34
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
34
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.9",
|
|
35
35
|
"typebox": "1.3.18"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
38
|
+
"@code-yeongyu/senpi": "2026.9.9"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
41
|
+
"@code-yeongyu/senpi": "2026.9.9"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"senpi",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@earendil-works/pi-agent-core",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.9",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"prepublishOnly": "npm run build"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.
|
|
39
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.9.
|
|
38
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.9",
|
|
39
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.9.9",
|
|
40
40
|
"diff": "9.0.0",
|
|
41
41
|
"ignore": "7.0.6",
|
|
42
42
|
"typebox": "1.3.18",
|
|
@@ -1618,11 +1618,19 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
|
|
|
1618
1618
|
disableThinkingForRequest(params, model, compat);
|
|
1619
1619
|
}
|
|
1620
1620
|
}
|
|
1621
|
-
//
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1624
|
-
//
|
|
1625
|
-
|
|
1621
|
+
// A budget-thinking request whose final assistant turn came from another API
|
|
1622
|
+
// and starts with tool_use (its thinking demoted to text or dropped) is the
|
|
1623
|
+
// history shape Anthropic has rejected with "final assistant message must
|
|
1624
|
+
// start with a thinking block", so that request degrades thinking instead of
|
|
1625
|
+
// failing every turn. Nothing else degrades: adaptive families accept a
|
|
1626
|
+
// tool_use-first final turn (verified live 2026-09-09 on claude-opus-5,
|
|
1627
|
+
// claude-opus-4-6 and claude-fable-5), the model itself skips thinking before
|
|
1628
|
+
// trivial tool calls, and every change to `thinking` re-keys the prompt
|
|
1629
|
+
// cache - degrading here re-wrote the whole cached prefix on each tool
|
|
1630
|
+
// continuation (the "cache misses every second prompt" report).
|
|
1631
|
+
if (params.thinking?.type === "enabled" &&
|
|
1632
|
+
finalAssistantTurnIsForeign(context.messages) &&
|
|
1633
|
+
finalAssistantTurnStartsWithToolUse(params.messages))
|
|
1626
1634
|
disableThinkingForRequest(params, model, compat);
|
|
1627
1635
|
if (options?.metadata) {
|
|
1628
1636
|
const userId = options.metadata.user_id;
|
|
@@ -1663,6 +1671,16 @@ function applyExtraBodyToAnthropicParams(params, extraBody) {
|
|
|
1663
1671
|
* thinking/redacted_thinking block. Anthropic requires thinking-enabled
|
|
1664
1672
|
* requests to start that turn with a thinking block.
|
|
1665
1673
|
*/
|
|
1674
|
+
/** True when the last assistant message was produced through a different wire API. */
|
|
1675
|
+
function finalAssistantTurnIsForeign(messages) {
|
|
1676
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1677
|
+
const message = messages[index];
|
|
1678
|
+
if (message.role !== "assistant")
|
|
1679
|
+
continue;
|
|
1680
|
+
return message.api !== "anthropic-messages";
|
|
1681
|
+
}
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1666
1684
|
function finalAssistantTurnStartsWithToolUse(messages) {
|
|
1667
1685
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1668
1686
|
const message = messages[index];
|