@rynx-ai/daemon 0.1.0 → 0.1.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/dist/browser-cli-args.d.ts +20 -0
- package/dist/browser-cli-args.js +100 -0
- package/dist/browser-runner-session-context.d.ts +24 -0
- package/dist/browser-runner-session-context.js +113 -0
- package/dist/cdp-keyboard-input.d.ts +6 -0
- package/dist/cdp-keyboard-input.js +209 -0
- package/dist/channel-store.d.ts +0 -6
- package/dist/channel-store.js +2 -18
- package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
- package/dist/chrome-for-testing-release-resolver.js +214 -0
- package/dist/chrome-for-testing-store.d.ts +85 -0
- package/dist/chrome-for-testing-store.js +836 -0
- package/dist/cli.js +467 -30
- package/dist/control-client.d.ts +113 -0
- package/dist/control-client.js +792 -0
- package/dist/control-deps.js +37 -6
- package/dist/control-endpoint.d.ts +14 -0
- package/dist/control-endpoint.js +124 -0
- package/dist/control-link-store.d.ts +16 -0
- package/dist/control-link-store.js +53 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +12 -2
- package/dist/daemon-server.js +559 -28
- package/dist/db.js +140 -10
- package/dist/desktop-browser-host-client.d.ts +34 -0
- package/dist/desktop-browser-host-client.js +413 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +127 -0
- package/dist/direct-runtime-config.d.ts +20 -0
- package/dist/direct-runtime-config.js +101 -0
- package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
- package/dist/embedded-browser-profile-cleanup.js +338 -0
- package/dist/headless-browser-host.d.ts +14 -0
- package/dist/headless-browser-host.js +2156 -0
- package/dist/index-daemon.js +28 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
- package/dist/migrations/cleanup-legacy-sessions.js +10 -28
- package/dist/plugin-cli.d.ts +30 -0
- package/dist/plugin-cli.js +270 -0
- package/dist/plugin-host-rpc.d.ts +85 -0
- package/dist/plugin-host-rpc.js +878 -0
- package/dist/plugin-install-restart.d.ts +13 -0
- package/dist/plugin-install-restart.js +24 -0
- package/dist/plugin-installer.d.ts +36 -15
- package/dist/plugin-installer.js +420 -155
- package/dist/plugin-package.d.ts +38 -0
- package/dist/plugin-package.js +553 -0
- package/dist/plugin-runtime-control.d.ts +2 -0
- package/dist/plugin-runtime-control.js +37 -0
- package/dist/plugin-store.d.ts +37 -8
- package/dist/plugin-store.js +80 -15
- package/dist/plugin-supervisor.d.ts +160 -0
- package/dist/plugin-supervisor.js +1143 -0
- package/dist/pm2.d.ts +3 -1
- package/dist/pm2.js +55 -13
- package/dist/provider-cli-service.d.ts +46 -0
- package/dist/provider-cli-service.js +510 -0
- package/dist/registry.d.ts +6 -19
- package/dist/registry.js +22 -87
- package/dist/remote-runtime-access-store.d.ts +61 -0
- package/dist/remote-runtime-access-store.js +386 -0
- package/dist/remote-runtime-admin.d.ts +32 -0
- package/dist/remote-runtime-admin.js +107 -0
- package/dist/remote-runtime-connection-manager.d.ts +92 -0
- package/dist/remote-runtime-connection-manager.js +1409 -0
- package/dist/remote-runtime-credential-store.d.ts +45 -0
- package/dist/remote-runtime-credential-store.js +639 -0
- package/dist/remote-runtime-target-control.d.ts +47 -0
- package/dist/remote-runtime-target-control.js +433 -0
- package/dist/remote-runtime-target-store.d.ts +53 -0
- package/dist/remote-runtime-target-store.js +307 -0
- package/dist/runtime-share-addresses.d.ts +21 -0
- package/dist/runtime-share-addresses.js +50 -0
- package/dist/session-browser-capability-store.d.ts +13 -0
- package/dist/session-browser-capability-store.js +58 -0
- package/dist/session-emulator-binding-store.d.ts +10 -0
- package/dist/session-emulator-binding-store.js +61 -0
- package/dist/session-launch-operations.d.ts +41 -0
- package/dist/session-launch-operations.js +124 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +57 -7
- package/dist/setup.js +9 -4
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +18 -10
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { AgentRuntimeError, agentSpecSchema, SteerController, } from "@rynx-ai/core";
|
|
3
|
+
import { PLUGIN_HOST_MAX_ACTIVE_RUNS, } from "@rynx-ai/plugin-sdk";
|
|
4
|
+
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
5
|
+
import { ensurePluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
|
|
6
|
+
import { createControlLink } from "./control-link-store.js";
|
|
7
|
+
const MAX_MESSAGE_LENGTH = 1_000_000;
|
|
8
|
+
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
9
|
+
export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
|
|
10
|
+
const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
|
|
11
|
+
const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
|
|
12
|
+
const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
|
|
13
|
+
const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
14
|
+
const METHOD_CAPABILITY = {
|
|
15
|
+
"installation.proof.sign": "installation.proof",
|
|
16
|
+
"agent.catalog.list": "agent.catalog.read",
|
|
17
|
+
"agent.catalog.get": "agent.catalog.read",
|
|
18
|
+
"agent.execution.resolve": "agent.catalog.read",
|
|
19
|
+
"session.ensure": "session.provision",
|
|
20
|
+
"session.run": "conversation.run",
|
|
21
|
+
"session.steer": "conversation.run",
|
|
22
|
+
"session.interrupt": "session.cancel",
|
|
23
|
+
"session.interaction.resolve": "conversation.run",
|
|
24
|
+
"session.models": "runtime.catalog.read",
|
|
25
|
+
"session.binding.get": "session.binding.read",
|
|
26
|
+
"session.binding.list": "session.binding.read",
|
|
27
|
+
"session.binding.find": "session.binding.read",
|
|
28
|
+
"session.goal.get": "session.goal.read",
|
|
29
|
+
"session.goal.set": "session.goal.write",
|
|
30
|
+
"session.goal.clear": "session.goal.write",
|
|
31
|
+
"session.fork": "session.fork",
|
|
32
|
+
"session.meta.get": "session.meta.read",
|
|
33
|
+
"session.meta.list": "session.meta.read",
|
|
34
|
+
"session.meta.setTitle": "session.meta.title.write",
|
|
35
|
+
"control.link.issue": "control-link.issue",
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* One authenticated plugin's capability-gated Host RPC surface.
|
|
39
|
+
*
|
|
40
|
+
* This adapter is deliberately the only place that maps public string methods
|
|
41
|
+
* to trusted runtime objects. The runner receives neither this class nor its
|
|
42
|
+
* dependencies; it receives JSON responses and canonical session events.
|
|
43
|
+
*/
|
|
44
|
+
export class PluginHostRpc {
|
|
45
|
+
pluginId;
|
|
46
|
+
daemonInstanceId;
|
|
47
|
+
grants;
|
|
48
|
+
services;
|
|
49
|
+
emit;
|
|
50
|
+
activeRuns = new Map();
|
|
51
|
+
activeSessionRuns = new Map();
|
|
52
|
+
activeRunsByOperation = new Map();
|
|
53
|
+
disposed = false;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.pluginId = requiredString(options.pluginId, "pluginId", 64);
|
|
56
|
+
this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
|
|
57
|
+
this.grants = new Set(options.grants);
|
|
58
|
+
this.services = options.services;
|
|
59
|
+
this.emit = options.emit;
|
|
60
|
+
}
|
|
61
|
+
async call(method, params) {
|
|
62
|
+
if (this.disposed)
|
|
63
|
+
throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
|
|
64
|
+
if (method === "host.info") {
|
|
65
|
+
return {
|
|
66
|
+
pluginId: this.pluginId,
|
|
67
|
+
installationId: this.services.installationId,
|
|
68
|
+
daemonInstanceId: this.daemonInstanceId,
|
|
69
|
+
installationProof: {
|
|
70
|
+
algorithm: this.services.installationProof.algorithm,
|
|
71
|
+
publicKey: this.services.installationProof.publicKey,
|
|
72
|
+
},
|
|
73
|
+
protocolVersion: 1,
|
|
74
|
+
grants: [...this.grants],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const capability = METHOD_CAPABILITY[method];
|
|
78
|
+
if (!capability)
|
|
79
|
+
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
80
|
+
if (!this.grants.has(capability)) {
|
|
81
|
+
throw new PluginHostCallError("CAPABILITY_DENIED", `host method ${method} requires capability ${capability}`);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
switch (method) {
|
|
85
|
+
case "installation.proof.sign": {
|
|
86
|
+
const challenge = requiredString(record(params).challenge, "challenge", 16_384);
|
|
87
|
+
return {
|
|
88
|
+
algorithm: this.services.installationProof.algorithm,
|
|
89
|
+
publicKey: this.services.installationProof.publicKey,
|
|
90
|
+
signature: this.services.installationProof.sign(this.pluginId, challenge),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
case "agent.catalog.list":
|
|
94
|
+
return await this.services.agentCatalog.list();
|
|
95
|
+
case "agent.catalog.get":
|
|
96
|
+
return await this.services.agentCatalog.get(requiredAgentId(record(params).id, "id"));
|
|
97
|
+
case "agent.execution.resolve":
|
|
98
|
+
return await this.resolveAgentExecution(params);
|
|
99
|
+
case "session.ensure":
|
|
100
|
+
return this.ensureSession(params);
|
|
101
|
+
case "session.run":
|
|
102
|
+
return await this.runSession(params);
|
|
103
|
+
case "session.steer":
|
|
104
|
+
return this.steerSession(params);
|
|
105
|
+
case "session.interrupt":
|
|
106
|
+
return await this.interruptRun(params);
|
|
107
|
+
case "session.interaction.resolve":
|
|
108
|
+
return await this.resolveSessionInteraction(params);
|
|
109
|
+
case "session.models":
|
|
110
|
+
return await this.services.capabilities.listModels(optionalRuntime(params));
|
|
111
|
+
case "session.binding.get":
|
|
112
|
+
return await this.getBinding(params);
|
|
113
|
+
case "session.binding.list":
|
|
114
|
+
return await this.listBindings();
|
|
115
|
+
case "session.binding.find":
|
|
116
|
+
return await this.findBinding(params);
|
|
117
|
+
case "session.goal.get":
|
|
118
|
+
return await this.withOwnedSession(params, (id) => this.services.capabilities.getGoal(id));
|
|
119
|
+
case "session.goal.set": {
|
|
120
|
+
const input = record(params);
|
|
121
|
+
const objective = requiredText(input.objective, "objective", 100_000);
|
|
122
|
+
return await this.withOwnedSession(input, (id) => this.services.capabilities.setGoal(id, objective));
|
|
123
|
+
}
|
|
124
|
+
case "session.goal.clear":
|
|
125
|
+
return await this.withOwnedSession(params, (id) => this.services.capabilities.clearGoal(id));
|
|
126
|
+
case "session.fork":
|
|
127
|
+
return await this.forkSession(params);
|
|
128
|
+
case "session.meta.get":
|
|
129
|
+
return this.ownedSession(requiredSessionId(params));
|
|
130
|
+
case "session.meta.list":
|
|
131
|
+
return this.services.sessionRegistry.list().filter((session) => this.isOwned(session));
|
|
132
|
+
case "session.meta.setTitle":
|
|
133
|
+
return this.setSessionTitle(params);
|
|
134
|
+
case "control.link.issue":
|
|
135
|
+
return this.issueControlLink(params);
|
|
136
|
+
default:
|
|
137
|
+
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
throw toHostCallError(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async dispose() {
|
|
145
|
+
if (this.disposed)
|
|
146
|
+
return;
|
|
147
|
+
this.disposed = true;
|
|
148
|
+
const runs = [...this.activeRuns.values()];
|
|
149
|
+
this.activeRuns.clear();
|
|
150
|
+
this.activeSessionRuns.clear();
|
|
151
|
+
this.activeRunsByOperation.clear();
|
|
152
|
+
for (const run of runs) {
|
|
153
|
+
run.interruptRequested = true;
|
|
154
|
+
if (run.interruptResult && await run.interruptResult.catch(() => false))
|
|
155
|
+
continue;
|
|
156
|
+
if (this.services.interruptSession) {
|
|
157
|
+
await this.services.interruptSession(run.sessionId).catch(() => false);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
run.abort.abort();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
ensureSession(params) {
|
|
165
|
+
const input = record(params);
|
|
166
|
+
const scopeId = requiredString(input.scopeId, "scopeId", 512);
|
|
167
|
+
const operationId = requiredString(input.operationId, "operationId", 512);
|
|
168
|
+
const session = sessionLaunchMeta(input.session);
|
|
169
|
+
const requestHash = hashCanonicalJson(session);
|
|
170
|
+
return ensurePluginSessionLaunch({
|
|
171
|
+
pluginId: this.pluginId,
|
|
172
|
+
scopeId,
|
|
173
|
+
operationId,
|
|
174
|
+
requestHash,
|
|
175
|
+
session,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async runSession(params) {
|
|
179
|
+
const input = record(params);
|
|
180
|
+
const sessionId = requiredSessionId(input);
|
|
181
|
+
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
182
|
+
const session = this.ownedSession(sessionId);
|
|
183
|
+
if (operationId && this.activeRunsByOperation.has(operationId)) {
|
|
184
|
+
throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already has an active run: ${operationId}`, true);
|
|
185
|
+
}
|
|
186
|
+
if (this.activeSessionRuns.has(sessionId)) {
|
|
187
|
+
throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `session already has an active run: ${sessionId}`, true);
|
|
188
|
+
}
|
|
189
|
+
if (this.activeRuns.size >= PLUGIN_HOST_MAX_ACTIVE_RUNS) {
|
|
190
|
+
throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
|
|
191
|
+
}
|
|
192
|
+
const message = requiredText(input.message, "message", MAX_MESSAGE_LENGTH);
|
|
193
|
+
const abort = new AbortController();
|
|
194
|
+
const steer = new SteerController();
|
|
195
|
+
const runId = randomUUID();
|
|
196
|
+
const run = {
|
|
197
|
+
runId,
|
|
198
|
+
sessionId,
|
|
199
|
+
operationId,
|
|
200
|
+
interruptRequested: false,
|
|
201
|
+
abort,
|
|
202
|
+
steer,
|
|
203
|
+
};
|
|
204
|
+
this.activeRuns.set(runId, run);
|
|
205
|
+
this.activeSessionRuns.set(sessionId, run);
|
|
206
|
+
if (operationId)
|
|
207
|
+
this.activeRunsByOperation.set(operationId, run);
|
|
208
|
+
let started;
|
|
209
|
+
try {
|
|
210
|
+
started = await this.services.conversationRuntime.runLive({
|
|
211
|
+
message,
|
|
212
|
+
threadId: sessionId,
|
|
213
|
+
provider: optionalRuntimeValue(input.provider),
|
|
214
|
+
model: optionalString(input.model, "model", 256) ?? session.model,
|
|
215
|
+
reasoningEffort: optionalString(input.reasoningEffort, "reasoningEffort", 128) ?? session.reasoningEffort,
|
|
216
|
+
cwd: optionalString(input.cwd, "cwd", 16_384),
|
|
217
|
+
agentName: optionalAgentId(input.agent, "agent") ?? session.agent,
|
|
218
|
+
agentSpec: session.config,
|
|
219
|
+
signal: abort.signal,
|
|
220
|
+
steer,
|
|
221
|
+
developerInstructions: optionalString(input.developerInstructions, "developerInstructions", 200_000),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
this.releaseRun(run);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
if (this.disposed || this.activeRuns.get(runId) !== run) {
|
|
229
|
+
this.releaseRun(run);
|
|
230
|
+
throw new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
|
|
231
|
+
}
|
|
232
|
+
// Let the RPC transport write the request response before stream events are
|
|
233
|
+
// emitted. `runLive` has already subscribed, so no agent event is lost.
|
|
234
|
+
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
235
|
+
return {
|
|
236
|
+
runId,
|
|
237
|
+
sessionId,
|
|
238
|
+
provider: started.provider,
|
|
239
|
+
model: started.model,
|
|
240
|
+
authSource: started.authSource,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async pumpRun(run, generator) {
|
|
244
|
+
let terminalFailure;
|
|
245
|
+
try {
|
|
246
|
+
for await (const event of generator) {
|
|
247
|
+
if (this.disposed || this.activeRuns.get(run.runId) !== run)
|
|
248
|
+
return;
|
|
249
|
+
if (await this.interruptWon(run))
|
|
250
|
+
return;
|
|
251
|
+
if (this.disposed || this.activeRuns.get(run.runId) !== run)
|
|
252
|
+
return;
|
|
253
|
+
const finalResponse = topLevelAssistantResponse(event);
|
|
254
|
+
if (finalResponse !== undefined) {
|
|
255
|
+
run.finalAssistantResponse = truncateUtf8(finalResponse, MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES);
|
|
256
|
+
}
|
|
257
|
+
for (const projected of projectSessionEvent(run, event)) {
|
|
258
|
+
await this.emit(projected);
|
|
259
|
+
}
|
|
260
|
+
terminalFailure = sessionRunFailure(event);
|
|
261
|
+
if (terminalFailure)
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
if (!this.disposed &&
|
|
265
|
+
this.activeRuns.get(run.runId) === run &&
|
|
266
|
+
!(await this.interruptWon(run))) {
|
|
267
|
+
if (terminalFailure) {
|
|
268
|
+
await this.emit({
|
|
269
|
+
event: "session.run.failed",
|
|
270
|
+
payload: {
|
|
271
|
+
runId: run.runId,
|
|
272
|
+
sessionId: run.sessionId,
|
|
273
|
+
error: terminalFailure,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
await this.emit({
|
|
279
|
+
event: "session.run.completed",
|
|
280
|
+
payload: {
|
|
281
|
+
runId: run.runId,
|
|
282
|
+
sessionId: run.sessionId,
|
|
283
|
+
...(run.finalAssistantResponse
|
|
284
|
+
? { finalResponse: run.finalAssistantResponse }
|
|
285
|
+
: {}),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (!this.disposed &&
|
|
293
|
+
this.activeRuns.get(run.runId) === run &&
|
|
294
|
+
!(await this.interruptWon(run))) {
|
|
295
|
+
const failure = toHostCallError(error);
|
|
296
|
+
if (failure.code === "INTERACTION_EVENT_TOO_LARGE") {
|
|
297
|
+
// A blocking control-flow event must never be projected lossily. End
|
|
298
|
+
// the native Turn before telling the plugin it failed, otherwise the
|
|
299
|
+
// invisible waiter would keep the daemon slot occupied forever.
|
|
300
|
+
const interrupted = (await this.services.interruptSession?.(run.sessionId).catch(() => false)) ?? false;
|
|
301
|
+
if (!interrupted)
|
|
302
|
+
run.abort.abort();
|
|
303
|
+
}
|
|
304
|
+
await Promise.resolve(this.emit({
|
|
305
|
+
event: "session.run.failed",
|
|
306
|
+
payload: {
|
|
307
|
+
runId: run.runId,
|
|
308
|
+
sessionId: run.sessionId,
|
|
309
|
+
error: {
|
|
310
|
+
code: failure.code,
|
|
311
|
+
message: truncateUtf8(failure.message, 16 * 1024),
|
|
312
|
+
retryable: failure.retryable,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
})).catch(() => undefined);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
this.releaseRun(run);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
releaseRun(run) {
|
|
323
|
+
if (this.activeRuns.get(run.runId) === run)
|
|
324
|
+
this.activeRuns.delete(run.runId);
|
|
325
|
+
if (this.activeSessionRuns.get(run.sessionId) === run) {
|
|
326
|
+
this.activeSessionRuns.delete(run.sessionId);
|
|
327
|
+
}
|
|
328
|
+
if (run.operationId && this.activeRunsByOperation.get(run.operationId) === run) {
|
|
329
|
+
this.activeRunsByOperation.delete(run.operationId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
steerSession(params) {
|
|
333
|
+
const input = record(params);
|
|
334
|
+
const run = this.activeRun(input);
|
|
335
|
+
const text = requiredText(input.text, "text", MAX_MESSAGE_LENGTH);
|
|
336
|
+
return { delivered: run.steer.send(text) };
|
|
337
|
+
}
|
|
338
|
+
async interruptRun(params) {
|
|
339
|
+
const input = record(params);
|
|
340
|
+
const run = this.activeRunByIdentity(input);
|
|
341
|
+
if (run.interruptResult) {
|
|
342
|
+
return { interrupted: await run.interruptResult };
|
|
343
|
+
}
|
|
344
|
+
// Serialize cancellation with stream projection. Events observed while
|
|
345
|
+
// the authoritative stop is pending wait for its result: true suppresses
|
|
346
|
+
// them, while false/error lets genuine native progress/terminal continue.
|
|
347
|
+
run.interruptRequested = true;
|
|
348
|
+
const interruptResult = this.services.interruptSession
|
|
349
|
+
// ConversationRuntime wires AbortSignal to this same native operation.
|
|
350
|
+
// Invoke it directly once so this boolean is the authoritative result;
|
|
351
|
+
// aborting first would trigger a duplicate, unobserved interrupt.
|
|
352
|
+
? Promise.resolve().then(() => this.services.interruptSession(run.sessionId))
|
|
353
|
+
: Promise.resolve().then(() => {
|
|
354
|
+
run.abort.abort();
|
|
355
|
+
// Without a result-bearing Host interrupt, AbortSignal is best
|
|
356
|
+
// effort and cannot prove that native execution stopped.
|
|
357
|
+
return false;
|
|
358
|
+
});
|
|
359
|
+
run.interruptResult = interruptResult;
|
|
360
|
+
try {
|
|
361
|
+
const interrupted = await interruptResult;
|
|
362
|
+
if (!interrupted && this.activeRuns.get(run.runId) === run) {
|
|
363
|
+
run.interruptRequested = false;
|
|
364
|
+
run.interruptResult = undefined;
|
|
365
|
+
}
|
|
366
|
+
return { interrupted };
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
if (this.activeRuns.get(run.runId) === run) {
|
|
370
|
+
run.interruptRequested = false;
|
|
371
|
+
run.interruptResult = undefined;
|
|
372
|
+
}
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async interruptWon(run) {
|
|
377
|
+
if (!run.interruptRequested)
|
|
378
|
+
return false;
|
|
379
|
+
if (!run.interruptResult)
|
|
380
|
+
return true;
|
|
381
|
+
return run.interruptResult.catch(() => false);
|
|
382
|
+
}
|
|
383
|
+
activeRun(input) {
|
|
384
|
+
const runId = requiredString(input.runId, "runId", 128);
|
|
385
|
+
const run = this.activeRuns.get(runId);
|
|
386
|
+
if (!run)
|
|
387
|
+
throw new PluginHostCallError("RUN_NOT_FOUND", `active run not found: ${runId}`);
|
|
388
|
+
return run;
|
|
389
|
+
}
|
|
390
|
+
activeRunByIdentity(input) {
|
|
391
|
+
const runId = optionalString(input.runId, "runId", 128);
|
|
392
|
+
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
393
|
+
if ((runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
|
|
394
|
+
throw new PluginHostCallError("INVALID_PARAMS", "exactly one of runId or operationId is required");
|
|
395
|
+
}
|
|
396
|
+
const run = runId
|
|
397
|
+
? this.activeRuns.get(runId)
|
|
398
|
+
: this.activeRunsByOperation.get(operationId);
|
|
399
|
+
if (!run) {
|
|
400
|
+
throw new PluginHostCallError("RUN_NOT_FOUND", `active run not found: ${runId ?? operationId}`);
|
|
401
|
+
}
|
|
402
|
+
return run;
|
|
403
|
+
}
|
|
404
|
+
async getBinding(params) {
|
|
405
|
+
const sessionId = requiredSessionId(params);
|
|
406
|
+
this.ownedSession(sessionId);
|
|
407
|
+
return await this.services.sessionStore.get(sessionId);
|
|
408
|
+
}
|
|
409
|
+
async listBindings() {
|
|
410
|
+
const owned = new Set(this.services.sessionRegistry.list().filter((session) => this.isOwned(session)).map((session) => session.id));
|
|
411
|
+
return (await this.services.sessionStore.listAll()).filter((binding) => owned.has(binding.localThreadId));
|
|
412
|
+
}
|
|
413
|
+
async findBinding(params) {
|
|
414
|
+
const input = record(params);
|
|
415
|
+
const runtimeSessionId = requiredString(input.runtimeSessionId, "runtimeSessionId", 512);
|
|
416
|
+
const binding = await this.services.sessionStore.findByCodexSessionId(runtimeSessionId);
|
|
417
|
+
if (!binding)
|
|
418
|
+
return null;
|
|
419
|
+
this.ownedSession(binding.localThreadId);
|
|
420
|
+
return binding;
|
|
421
|
+
}
|
|
422
|
+
async withOwnedSession(params, run) {
|
|
423
|
+
const sessionId = requiredSessionId(params);
|
|
424
|
+
this.ownedSession(sessionId);
|
|
425
|
+
return await run(sessionId);
|
|
426
|
+
}
|
|
427
|
+
async resolveSessionInteraction(params) {
|
|
428
|
+
const input = record(params);
|
|
429
|
+
const sessionId = requiredSessionId(input);
|
|
430
|
+
this.ownedSession(sessionId);
|
|
431
|
+
const interactionId = requiredString(input.interactionId, "interactionId", 512);
|
|
432
|
+
const resolution = sessionInteractionResolution(input.resolution);
|
|
433
|
+
const resolve = this.services.resolveSessionInteraction;
|
|
434
|
+
if (!resolve) {
|
|
435
|
+
return { disposition: "not_found" };
|
|
436
|
+
}
|
|
437
|
+
return await resolve(sessionId, interactionId, resolution);
|
|
438
|
+
}
|
|
439
|
+
async forkSession(params) {
|
|
440
|
+
const input = record(params);
|
|
441
|
+
const fromSessionId = requiredString(input.fromSessionId, "fromSessionId", 512);
|
|
442
|
+
const toSessionId = requiredString(input.toSessionId, "toSessionId", 512);
|
|
443
|
+
this.ownedSession(fromSessionId);
|
|
444
|
+
this.ownedSession(toSessionId);
|
|
445
|
+
return await this.services.capabilities.forkSession(fromSessionId, toSessionId);
|
|
446
|
+
}
|
|
447
|
+
setSessionTitle(params) {
|
|
448
|
+
const input = record(params);
|
|
449
|
+
const sessionId = requiredSessionId(input);
|
|
450
|
+
this.ownedSession(sessionId);
|
|
451
|
+
const title = requiredText(input.title, "title", 1_000);
|
|
452
|
+
this.services.sessionRegistry.setTitle(sessionId, title);
|
|
453
|
+
return { updated: true };
|
|
454
|
+
}
|
|
455
|
+
resolveAgentExecution(params) {
|
|
456
|
+
const input = record(params);
|
|
457
|
+
const defaultAgent = optionalAgentId(input.defaultAgent, "defaultAgent");
|
|
458
|
+
return this.services.agentCatalog.resolveExecution({
|
|
459
|
+
...(defaultAgent ? { defaultAgent } : {}),
|
|
460
|
+
session: sessionExecutionSnapshot(input.session),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
issueControlLink(params) {
|
|
464
|
+
const input = record(params);
|
|
465
|
+
const sessionId = requiredSessionId(input);
|
|
466
|
+
this.ownedSession(sessionId);
|
|
467
|
+
const ttlSeconds = optionalPositiveNumber(input.ttlSeconds, "ttlSeconds");
|
|
468
|
+
return createControlLink({
|
|
469
|
+
pluginId: this.pluginId,
|
|
470
|
+
sessionId,
|
|
471
|
+
...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
ownedSession(sessionId) {
|
|
475
|
+
const session = this.services.sessionRegistry.get(sessionId);
|
|
476
|
+
if (!session || !this.isOwned(session)) {
|
|
477
|
+
throw new PluginHostCallError("SESSION_NOT_FOUND", `plugin session not found: ${sessionId}`);
|
|
478
|
+
}
|
|
479
|
+
return session;
|
|
480
|
+
}
|
|
481
|
+
isOwned(session) {
|
|
482
|
+
return session.source === `plugin:${this.pluginId}`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
export class PluginHostCallError extends PluginRunnerError {
|
|
486
|
+
retryable;
|
|
487
|
+
data;
|
|
488
|
+
constructor(code, message, retryable = false, data) {
|
|
489
|
+
super(code, message, {
|
|
490
|
+
retryable,
|
|
491
|
+
...(data === undefined ? {} : { data }),
|
|
492
|
+
});
|
|
493
|
+
this.retryable = retryable;
|
|
494
|
+
this.data = data;
|
|
495
|
+
this.name = "PluginHostCallError";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function toHostCallError(error) {
|
|
499
|
+
if (error instanceof PluginHostCallError)
|
|
500
|
+
return error;
|
|
501
|
+
if (error instanceof AgentRuntimeError && error.code === "live_unavailable") {
|
|
502
|
+
// ConversationRuntime emits this code only before injectMessage is called:
|
|
503
|
+
// the native live Session (including its exact Skill environment) could
|
|
504
|
+
// not be brought up, so callers may fail without quarantining capacity.
|
|
505
|
+
return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
|
|
506
|
+
}
|
|
507
|
+
if (error instanceof SessionLaunchOperationConflictError) {
|
|
508
|
+
return new PluginHostCallError(error.code, error.message, false);
|
|
509
|
+
}
|
|
510
|
+
if (error instanceof SessionLaunchOperationTargetDeletedError) {
|
|
511
|
+
return new PluginHostCallError(error.code, error.message, false, { sessionId: error.sessionId });
|
|
512
|
+
}
|
|
513
|
+
return new PluginHostCallError("HOST_CALL_FAILED", error instanceof Error ? error.message : String(error));
|
|
514
|
+
}
|
|
515
|
+
function sessionLaunchMeta(value) {
|
|
516
|
+
const input = record(value);
|
|
517
|
+
assertJsonValue(input, "session");
|
|
518
|
+
const agent = optionalAgentId(input.agent, "session.agent");
|
|
519
|
+
const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
|
|
520
|
+
const model = optionalString(input.model, "session.model", 256);
|
|
521
|
+
const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
|
|
522
|
+
const title = optionalString(input.title, "session.title", 1_000);
|
|
523
|
+
return {
|
|
524
|
+
...(agent ? { agent } : {}),
|
|
525
|
+
...(config ? { config } : {}),
|
|
526
|
+
...(model ? { model } : {}),
|
|
527
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
528
|
+
...(title ? { title } : {}),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function sessionExecutionSnapshot(value) {
|
|
532
|
+
const input = record(value);
|
|
533
|
+
const runtime = optionalRuntimeValue(input.runtime);
|
|
534
|
+
const model = optionalString(input.model, "session.model", 256);
|
|
535
|
+
const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
|
|
536
|
+
const agent = optionalAgentId(input.agent, "session.agent");
|
|
537
|
+
return {
|
|
538
|
+
...(runtime ? { runtime } : {}),
|
|
539
|
+
...(model ? { model } : {}),
|
|
540
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
541
|
+
...(agent ? { agent } : {}),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function requiredSessionId(params) {
|
|
545
|
+
return requiredString(record(params).sessionId, "sessionId", 512);
|
|
546
|
+
}
|
|
547
|
+
function sessionInteractionResolution(value) {
|
|
548
|
+
const input = record(value);
|
|
549
|
+
const keys = Object.keys(input);
|
|
550
|
+
if (keys.some((key) => key !== "actionId" && key !== "answers")) {
|
|
551
|
+
throw new PluginHostCallError("INVALID_PARAMS", "resolution contains unknown fields");
|
|
552
|
+
}
|
|
553
|
+
const actionId = requiredString(input.actionId, "resolution.actionId", 256);
|
|
554
|
+
if (input.answers === undefined)
|
|
555
|
+
return { actionId };
|
|
556
|
+
const answersInput = record(input.answers);
|
|
557
|
+
const entries = Object.entries(answersInput);
|
|
558
|
+
if (entries.length > 64) {
|
|
559
|
+
throw new PluginHostCallError("INVALID_PARAMS", "resolution.answers has too many fields");
|
|
560
|
+
}
|
|
561
|
+
const answers = {};
|
|
562
|
+
for (const [key, answer] of entries) {
|
|
563
|
+
if (!key || key.length > 256) {
|
|
564
|
+
throw new PluginHostCallError("INVALID_PARAMS", "resolution answer id is invalid");
|
|
565
|
+
}
|
|
566
|
+
if (typeof answer === "string") {
|
|
567
|
+
if (answer.length > 65_536) {
|
|
568
|
+
throw new PluginHostCallError("INVALID_PARAMS", `resolution.answers.${key} is too long`);
|
|
569
|
+
}
|
|
570
|
+
answers[key] = answer;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (!Array.isArray(answer) ||
|
|
574
|
+
answer.length > 64 ||
|
|
575
|
+
answer.some((item) => typeof item !== "string" || item.length > 65_536)) {
|
|
576
|
+
throw new PluginHostCallError("INVALID_PARAMS", `resolution.answers.${key} must be a bounded string array`);
|
|
577
|
+
}
|
|
578
|
+
answers[key] = [...answer];
|
|
579
|
+
}
|
|
580
|
+
const resolution = { actionId, answers };
|
|
581
|
+
if (jsonBytes(resolution) > SESSION_INTERACTION_RESOLUTION_MAX_BYTES) {
|
|
582
|
+
throw new PluginHostCallError("INVALID_PARAMS", "resolution is too large");
|
|
583
|
+
}
|
|
584
|
+
return resolution;
|
|
585
|
+
}
|
|
586
|
+
function optionalRuntime(params) {
|
|
587
|
+
return optionalRuntimeValue(record(params).runtime);
|
|
588
|
+
}
|
|
589
|
+
function optionalRuntimeValue(value) {
|
|
590
|
+
if (value === undefined || value === null || value === "")
|
|
591
|
+
return undefined;
|
|
592
|
+
if (value === "codex" || value === "traex" || value === "claude")
|
|
593
|
+
return value;
|
|
594
|
+
throw new PluginHostCallError("INVALID_PARAMS", "runtime must be codex, traex, or claude");
|
|
595
|
+
}
|
|
596
|
+
function record(value) {
|
|
597
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
598
|
+
throw new PluginHostCallError("INVALID_PARAMS", "host call params must be an object");
|
|
599
|
+
}
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
function requiredString(value, label, maxLength) {
|
|
603
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
604
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
|
|
605
|
+
}
|
|
606
|
+
if (value.length > maxLength) {
|
|
607
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is too long`);
|
|
608
|
+
}
|
|
609
|
+
return value.trim();
|
|
610
|
+
}
|
|
611
|
+
function requiredText(value, label, maxLength) {
|
|
612
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
613
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is required`);
|
|
614
|
+
}
|
|
615
|
+
if (value.length > maxLength) {
|
|
616
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is too long`);
|
|
617
|
+
}
|
|
618
|
+
return value;
|
|
619
|
+
}
|
|
620
|
+
function optionalString(value, label, maxLength) {
|
|
621
|
+
if (value === undefined || value === null || value === "")
|
|
622
|
+
return undefined;
|
|
623
|
+
if (typeof value !== "string") {
|
|
624
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a string`);
|
|
625
|
+
}
|
|
626
|
+
if (value.length > maxLength) {
|
|
627
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is too long`);
|
|
628
|
+
}
|
|
629
|
+
return value;
|
|
630
|
+
}
|
|
631
|
+
function requiredAgentId(value, label) {
|
|
632
|
+
const id = requiredString(value, label, 64);
|
|
633
|
+
if (!AGENT_ID_PATTERN.test(id)) {
|
|
634
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a valid agent id`);
|
|
635
|
+
}
|
|
636
|
+
return id;
|
|
637
|
+
}
|
|
638
|
+
function optionalAgentId(value, label) {
|
|
639
|
+
if (value === undefined || value === null || value === "")
|
|
640
|
+
return undefined;
|
|
641
|
+
return requiredAgentId(value, label);
|
|
642
|
+
}
|
|
643
|
+
function optionalPositiveNumber(value, label) {
|
|
644
|
+
if (value === undefined || value === null)
|
|
645
|
+
return undefined;
|
|
646
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
647
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a positive number`);
|
|
648
|
+
}
|
|
649
|
+
return value;
|
|
650
|
+
}
|
|
651
|
+
function assertJsonValue(value, label, seen = new Set()) {
|
|
652
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
653
|
+
return;
|
|
654
|
+
if (typeof value === "number") {
|
|
655
|
+
if (Number.isFinite(value))
|
|
656
|
+
return;
|
|
657
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must contain finite JSON numbers`);
|
|
658
|
+
}
|
|
659
|
+
if (typeof value !== "object") {
|
|
660
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must contain JSON data only`);
|
|
661
|
+
}
|
|
662
|
+
if (seen.has(value))
|
|
663
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must not be cyclic`);
|
|
664
|
+
seen.add(value);
|
|
665
|
+
if (Array.isArray(value)) {
|
|
666
|
+
value.forEach((item, index) => assertJsonValue(item, `${label}[${index}]`, seen));
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
for (const [key, item] of Object.entries(value)) {
|
|
670
|
+
if (item === undefined)
|
|
671
|
+
continue;
|
|
672
|
+
assertJsonValue(item, `${label}.${key}`, seen);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
seen.delete(value);
|
|
676
|
+
}
|
|
677
|
+
function hashCanonicalJson(value) {
|
|
678
|
+
return createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
679
|
+
}
|
|
680
|
+
function canonicalJson(value) {
|
|
681
|
+
if (value === null || typeof value !== "object")
|
|
682
|
+
return JSON.stringify(value);
|
|
683
|
+
if (Array.isArray(value))
|
|
684
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
685
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Project a canonical event onto the bounded plugin transport:
|
|
689
|
+
*
|
|
690
|
+
* - delta events are split into semantically equivalent delta events;
|
|
691
|
+
* - blocking interaction events must fit losslessly or fail the native Turn;
|
|
692
|
+
* - other oversized events are shape-preservingly compacted and marked;
|
|
693
|
+
* - if structural overhead alone is too large, a digest reference is emitted.
|
|
694
|
+
*
|
|
695
|
+
* The authoritative, unmodified event remains in the daemon's canonical
|
|
696
|
+
* session log. The reference is therefore a loss marker for a presentation
|
|
697
|
+
* plugin, not a replacement for host persistence.
|
|
698
|
+
*/
|
|
699
|
+
function projectSessionEvent(run, event) {
|
|
700
|
+
const direct = sessionHostEvent(run, event);
|
|
701
|
+
if (jsonBytes(direct) <= MAX_PLUGIN_HOST_EVENT_BYTES)
|
|
702
|
+
return [direct];
|
|
703
|
+
const input = jsonRecord(event);
|
|
704
|
+
if (isSessionInteractionEvent(input?.type)) {
|
|
705
|
+
throw new PluginHostCallError("INTERACTION_EVENT_TOO_LARGE", `canonical ${String(input?.type)} exceeds the lossless plugin transport budget`, false, { bytes: jsonBytes(direct), limit: MAX_PLUGIN_HOST_EVENT_BYTES });
|
|
706
|
+
}
|
|
707
|
+
if (typeof input?.delta === "string") {
|
|
708
|
+
const chunks = splitCodeUnits(input.delta, MAX_DELTA_CHUNK_CODE_UNITS);
|
|
709
|
+
const originalBytes = jsonBytes(event);
|
|
710
|
+
const projected = chunks.map((delta, index) => sessionHostEvent(run, { ...input, delta }, {
|
|
711
|
+
strategy: "chunk",
|
|
712
|
+
index,
|
|
713
|
+
count: chunks.length,
|
|
714
|
+
originalBytes,
|
|
715
|
+
}));
|
|
716
|
+
if (projected.every((item) => jsonBytes(item) <= MAX_PLUGIN_HOST_EVENT_BYTES)) {
|
|
717
|
+
return projected;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const originalJson = JSON.stringify(event);
|
|
721
|
+
const originalBytes = Buffer.byteLength(originalJson, "utf8");
|
|
722
|
+
const reference = createHash("sha256").update(originalJson).digest("hex");
|
|
723
|
+
const attempts = [
|
|
724
|
+
{ stringBytes: 64 * 1024, arrayItems: 256, objectKeys: 256 },
|
|
725
|
+
{ stringBytes: 16 * 1024, arrayItems: 64, objectKeys: 128 },
|
|
726
|
+
{ stringBytes: 4 * 1024, arrayItems: 16, objectKeys: 64 },
|
|
727
|
+
{ stringBytes: 1_024, arrayItems: 4, objectKeys: 32 },
|
|
728
|
+
{ stringBytes: 256, arrayItems: 1, objectKeys: 16 },
|
|
729
|
+
];
|
|
730
|
+
for (const limits of attempts) {
|
|
731
|
+
const compacted = compactJson(event, limits, 0);
|
|
732
|
+
const candidate = sessionHostEvent(run, compacted, {
|
|
733
|
+
strategy: "truncate",
|
|
734
|
+
originalBytes,
|
|
735
|
+
reference,
|
|
736
|
+
});
|
|
737
|
+
if (jsonBytes(candidate) <= MAX_PLUGIN_HOST_EVENT_BYTES)
|
|
738
|
+
return [candidate];
|
|
739
|
+
}
|
|
740
|
+
return [{
|
|
741
|
+
event: "session.run.event.reference",
|
|
742
|
+
payload: {
|
|
743
|
+
runId: run.runId,
|
|
744
|
+
sessionId: run.sessionId,
|
|
745
|
+
eventType: typeof input?.type === "string" ? input.type : "unknown",
|
|
746
|
+
originalBytes,
|
|
747
|
+
reference,
|
|
748
|
+
reason: "canonical event exceeded the plugin transport budget",
|
|
749
|
+
},
|
|
750
|
+
}];
|
|
751
|
+
}
|
|
752
|
+
function isSessionInteractionEvent(type) {
|
|
753
|
+
return type === "session.interaction.requested" ||
|
|
754
|
+
type === "session.interaction.resolved" ||
|
|
755
|
+
type === "session.interaction.cancelled";
|
|
756
|
+
}
|
|
757
|
+
/** Select only the last finalized, top-level assistant response. */
|
|
758
|
+
function topLevelAssistantResponse(event) {
|
|
759
|
+
const input = jsonRecord(event);
|
|
760
|
+
if (input?.type !== "response.output_item.done")
|
|
761
|
+
return undefined;
|
|
762
|
+
const item = jsonRecord(input.item);
|
|
763
|
+
if (item?.type !== "message" || item.parentToolCallId !== undefined)
|
|
764
|
+
return undefined;
|
|
765
|
+
const data = jsonRecord(item.data);
|
|
766
|
+
if (data?.role !== "assistant" || data.isMeta === true || !Array.isArray(data.content)) {
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
const text = data.content
|
|
770
|
+
.map((part) => jsonRecord(part))
|
|
771
|
+
.filter((part) => part?.type === "output_text" && typeof part.text === "string")
|
|
772
|
+
.map((part) => part.text)
|
|
773
|
+
.join("")
|
|
774
|
+
.trim();
|
|
775
|
+
return text || undefined;
|
|
776
|
+
}
|
|
777
|
+
function sessionRunFailure(event) {
|
|
778
|
+
const input = jsonRecord(event);
|
|
779
|
+
if (input?.type !== "response.failed")
|
|
780
|
+
return undefined;
|
|
781
|
+
const error = jsonRecord(input.error);
|
|
782
|
+
const code = typeof error?.code === "string" && error.code.trim()
|
|
783
|
+
? error.code
|
|
784
|
+
: "RUNTIME_TURN_FAILED";
|
|
785
|
+
const message = typeof error?.message === "string" && error.message.trim()
|
|
786
|
+
? error.message
|
|
787
|
+
: "Runtime Turn failed";
|
|
788
|
+
return {
|
|
789
|
+
code: truncateUtf8(code, 256),
|
|
790
|
+
message: truncateUtf8(message, 16 * 1024),
|
|
791
|
+
retryable: false,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function sessionHostEvent(run, event, delivery) {
|
|
795
|
+
return {
|
|
796
|
+
event: "session.run.event",
|
|
797
|
+
payload: {
|
|
798
|
+
runId: run.runId,
|
|
799
|
+
sessionId: run.sessionId,
|
|
800
|
+
event,
|
|
801
|
+
...(delivery ? { delivery } : {}),
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function jsonRecord(value) {
|
|
806
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
807
|
+
? value
|
|
808
|
+
: null;
|
|
809
|
+
}
|
|
810
|
+
function jsonBytes(value) {
|
|
811
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
812
|
+
}
|
|
813
|
+
function splitCodeUnits(value, maxCodeUnits) {
|
|
814
|
+
const chunks = [];
|
|
815
|
+
let offset = 0;
|
|
816
|
+
while (offset < value.length) {
|
|
817
|
+
let end = Math.min(value.length, offset + maxCodeUnits);
|
|
818
|
+
if (end < value.length && isHighSurrogate(value.charCodeAt(end - 1)))
|
|
819
|
+
end -= 1;
|
|
820
|
+
if (end === offset)
|
|
821
|
+
end += 1;
|
|
822
|
+
chunks.push(value.slice(offset, end));
|
|
823
|
+
offset = end;
|
|
824
|
+
}
|
|
825
|
+
return chunks.length > 0 ? chunks : [""];
|
|
826
|
+
}
|
|
827
|
+
function compactJson(value, limits, depth) {
|
|
828
|
+
if (value === null || typeof value === "boolean")
|
|
829
|
+
return value;
|
|
830
|
+
if (typeof value === "number")
|
|
831
|
+
return Number.isFinite(value) ? value : null;
|
|
832
|
+
if (typeof value === "string")
|
|
833
|
+
return truncateUtf8(value, limits.stringBytes);
|
|
834
|
+
if (depth >= 32)
|
|
835
|
+
return null;
|
|
836
|
+
if (Array.isArray(value)) {
|
|
837
|
+
return value
|
|
838
|
+
.slice(0, limits.arrayItems)
|
|
839
|
+
.map((item) => compactJson(item, limits, depth + 1));
|
|
840
|
+
}
|
|
841
|
+
if (typeof value === "object" && value !== null) {
|
|
842
|
+
const output = {};
|
|
843
|
+
for (const [key, item] of Object.entries(value).slice(0, limits.objectKeys)) {
|
|
844
|
+
if (item !== undefined)
|
|
845
|
+
output[key] = compactJson(item, limits, depth + 1);
|
|
846
|
+
}
|
|
847
|
+
return output;
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
function truncateUtf8(value, maxBytes) {
|
|
852
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
853
|
+
return value;
|
|
854
|
+
const marker = "…[truncated]";
|
|
855
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
856
|
+
if (maxBytes <= markerBytes)
|
|
857
|
+
return marker.slice(0, Math.max(0, maxBytes));
|
|
858
|
+
const target = maxBytes - markerBytes;
|
|
859
|
+
let low = 0;
|
|
860
|
+
let high = value.length;
|
|
861
|
+
while (low < high) {
|
|
862
|
+
let middle = Math.ceil((low + high) / 2);
|
|
863
|
+
if (middle < value.length && isHighSurrogate(value.charCodeAt(middle - 1)))
|
|
864
|
+
middle -= 1;
|
|
865
|
+
if (middle <= low)
|
|
866
|
+
middle = low + 1;
|
|
867
|
+
if (Buffer.byteLength(value.slice(0, middle), "utf8") <= target)
|
|
868
|
+
low = middle;
|
|
869
|
+
else
|
|
870
|
+
high = middle - 1;
|
|
871
|
+
}
|
|
872
|
+
if (low < value.length && isHighSurrogate(value.charCodeAt(low - 1)))
|
|
873
|
+
low -= 1;
|
|
874
|
+
return `${value.slice(0, Math.max(0, low))}${marker}`;
|
|
875
|
+
}
|
|
876
|
+
function isHighSurrogate(value) {
|
|
877
|
+
return value >= 0xd800 && value <= 0xdbff;
|
|
878
|
+
}
|