@rynx-ai/daemon 0.1.11-beta.4 → 0.1.11-beta.41
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/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +24 -18
- package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
- package/bundled-plugins/plugins/lark/dist/runtime.js +6043 -6016
- package/bundled-plugins/plugins/lark/dist/settings.js +6003 -5985
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
- package/bundled-plugins/plugins/lark/ui/index.html +2 -2
- package/dist/agent-file.js +0 -1
- package/dist/browser-artifact-management.js +4 -1
- package/dist/chrome-for-testing-http.d.ts +9 -0
- package/dist/chrome-for-testing-http.js +58 -0
- package/dist/chrome-for-testing-release-resolver.js +13 -6
- package/dist/chrome-for-testing-runtime.js +4 -3
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +99 -17
- package/dist/control-deps.d.ts +6 -2
- package/dist/control-deps.js +87 -4
- package/dist/control-link-store.js +4 -3
- package/dist/daemon-owner.js +4 -7
- package/dist/daemon-server.js +42 -17
- package/dist/db.d.ts +2 -2
- package/dist/db.js +157 -21
- package/dist/headless-browser-host.js +397 -41
- package/dist/lifecycle.d.ts +2 -1
- package/dist/lifecycle.js +4 -1
- package/dist/migrations/cleanup-legacy-sessions.js +4 -3
- package/dist/plugin-cli.d.ts +10 -0
- package/dist/plugin-cli.js +19 -2
- package/dist/plugin-host-rpc.d.ts +48 -9
- package/dist/plugin-host-rpc.js +593 -81
- package/dist/plugin-installer.js +3 -2
- package/dist/plugin-management-service.js +0 -2
- package/dist/plugin-marketplace.js +2 -1
- package/dist/plugin-store.js +2 -4
- package/dist/plugin-supervisor.js +0 -4
- package/dist/pm2.d.ts +2 -0
- package/dist/pm2.js +6 -0
- package/dist/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- package/dist/remote-runtime-connection-manager.js +25 -42
- package/dist/remote-runtime-target-store.d.ts +2 -2
- package/dist/remote-runtime-target-store.js +6 -6
- package/dist/session-emulator-binding-store.js +4 -6
- package/dist/session-fork-store.d.ts +1 -0
- package/dist/session-fork-store.js +85 -30
- package/dist/session-launch-operations.js +6 -5
- package/dist/session-log-store.d.ts +1 -0
- package/dist/session-log-store.js +46 -22
- package/dist/session-meta-store.d.ts +5 -0
- package/dist/session-meta-store.js +25 -2
- package/dist/session-portal-grant-store.d.ts +55 -0
- package/dist/session-portal-grant-store.js +160 -0
- package/dist/session-resource-store.d.ts +24 -6
- package/dist/session-resource-store.js +387 -69
- package/dist/session-timeline-projection-store.d.ts +24 -0
- package/dist/session-timeline-projection-store.js +53 -0
- package/dist/setup-service.d.ts +1 -0
- package/dist/setup-service.js +1 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +317 -10
- package/dist/sqlite.d.ts +22 -0
- package/dist/sqlite.js +57 -0
- package/dist/stdio-epipe-guard.d.ts +1 -1
- package/dist/stdio-epipe-guard.js +1 -1
- package/dist/system-dependencies.d.ts +22 -0
- package/dist/system-dependencies.js +79 -0
- package/package.json +17 -12
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
- /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
package/dist/plugin-host-rpc.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { realpath, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema,
|
|
4
|
+
import { AgentRuntimeError, agentSpecSchema, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
5
5
|
import { PLUGIN_HOST_MAX_ACTIVE_RUNS, } from "@rynx-ai/plugin-sdk";
|
|
6
|
+
import { parseRuntimeBrowserRequestHeadersGetParams, parseRuntimeBrowserRequestHeadersSetParams, } from "@rynx-ai/protocol/runtime-browser";
|
|
7
|
+
import { SessionBrowserServiceError, } from "@rynx-ai/server";
|
|
6
8
|
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
7
9
|
import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
|
|
8
10
|
import { createControlLink } from "./control-link-store.js";
|
|
11
|
+
import { issueSessionPortalTicket, revokeSessionPortalGrant, } from "./session-portal-grant-store.js";
|
|
12
|
+
import { claimSessionTimelineProjection, completeSessionTimelineProjection, } from "./session-timeline-projection-store.js";
|
|
9
13
|
const MAX_MESSAGE_LENGTH = 1_000_000;
|
|
10
14
|
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
11
15
|
export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
|
|
@@ -13,31 +17,59 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
|
|
|
13
17
|
const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
|
|
14
18
|
const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
|
|
15
19
|
const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
16
|
-
const METHOD_CAPABILITY = {
|
|
17
|
-
"installation.proof.sign": "installation.proof",
|
|
18
|
-
"agent.catalog.list": "agent.catalog.read",
|
|
19
|
-
"agent.catalog.get": "agent.catalog.read",
|
|
20
|
-
"agent.execution.resolve": "agent.catalog.read",
|
|
21
|
-
"session.ensure": "session.provision",
|
|
22
|
-
"session.run": "conversation.run",
|
|
23
|
-
"session.steer": "conversation.run",
|
|
24
|
-
"session.interrupt": "session.cancel",
|
|
25
|
-
"session.interaction.resolve": "conversation.run",
|
|
26
|
-
"session.models": "runtime.catalog.read",
|
|
27
|
-
"session.binding.get": "session.binding.read",
|
|
28
|
-
"session.binding.list": "session.binding.read",
|
|
29
|
-
"session.binding.find": "session.binding.read",
|
|
30
|
-
"session.goal.get": "session.goal.read",
|
|
31
|
-
"session.goal.set": "session.goal.write",
|
|
32
|
-
"session.goal.clear": "session.goal.write",
|
|
33
|
-
"session.fork": "session.fork",
|
|
34
|
-
"session.meta.get": "session.meta.read",
|
|
35
|
-
"session.meta.list": "session.meta.read",
|
|
36
|
-
"session.meta.setTitle": "session.meta.title.write",
|
|
37
|
-
"control.link.issue": "control-link.issue",
|
|
38
|
-
};
|
|
39
20
|
/**
|
|
40
|
-
*
|
|
21
|
+
* Buffers messages that arrive while the first `session.run` is still bringing
|
|
22
|
+
* up its native Turn. Once Core subscribes, later messages are delivered
|
|
23
|
+
* through the same input channel. After that subscription closes, this Run no
|
|
24
|
+
* longer accepts input; the caller waits for release and opens the next Run.
|
|
25
|
+
*/
|
|
26
|
+
class PluginRunInputController {
|
|
27
|
+
listener;
|
|
28
|
+
pending = new Map();
|
|
29
|
+
subscribed = false;
|
|
30
|
+
closed = false;
|
|
31
|
+
enqueue(text) {
|
|
32
|
+
if (this.closed || (this.subscribed && !this.listener)) {
|
|
33
|
+
return { accepted: false, cancel: () => undefined };
|
|
34
|
+
}
|
|
35
|
+
if (this.listener) {
|
|
36
|
+
this.listener(text);
|
|
37
|
+
return { accepted: true, cancel: () => undefined };
|
|
38
|
+
}
|
|
39
|
+
const token = Symbol("plugin-run-input");
|
|
40
|
+
this.pending.set(token, text);
|
|
41
|
+
return {
|
|
42
|
+
accepted: true,
|
|
43
|
+
cancel: () => {
|
|
44
|
+
this.pending.delete(token);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
if (this.closed)
|
|
50
|
+
return () => undefined;
|
|
51
|
+
if (this.listener) {
|
|
52
|
+
throw new Error("plugin Run input already has a subscriber");
|
|
53
|
+
}
|
|
54
|
+
this.subscribed = true;
|
|
55
|
+
this.listener = listener;
|
|
56
|
+
const queued = [...this.pending.values()];
|
|
57
|
+
this.pending.clear();
|
|
58
|
+
for (const text of queued)
|
|
59
|
+
listener(text);
|
|
60
|
+
return () => {
|
|
61
|
+
if (this.listener === listener)
|
|
62
|
+
this.listener = undefined;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
close() {
|
|
66
|
+
this.closed = true;
|
|
67
|
+
this.listener = undefined;
|
|
68
|
+
this.pending.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One authenticated plugin's Host RPC surface.
|
|
41
73
|
*
|
|
42
74
|
* This adapter is deliberately the only place that maps public string methods
|
|
43
75
|
* to trusted runtime objects. The runner receives neither this class nor its
|
|
@@ -49,7 +81,6 @@ export class PluginHostRpc {
|
|
|
49
81
|
daemonInstanceId;
|
|
50
82
|
productVersion;
|
|
51
83
|
cliVersion;
|
|
52
|
-
capabilities;
|
|
53
84
|
services;
|
|
54
85
|
managedWorkspaceRoot;
|
|
55
86
|
emit;
|
|
@@ -65,7 +96,6 @@ export class PluginHostRpc {
|
|
|
65
96
|
this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
|
|
66
97
|
this.productVersion = requiredString(options.productVersion, "productVersion", 128);
|
|
67
98
|
this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
|
|
68
|
-
this.capabilities = new Set(options.capabilities);
|
|
69
99
|
this.services = options.services;
|
|
70
100
|
this.managedWorkspaceRoot = options.managedWorkspaceRoot
|
|
71
101
|
? path.resolve(options.managedWorkspaceRoot)
|
|
@@ -89,15 +119,27 @@ export class PluginHostRpc {
|
|
|
89
119
|
publicKey: this.services.installationProof.publicKey,
|
|
90
120
|
},
|
|
91
121
|
protocolVersion: 1,
|
|
92
|
-
capabilities:
|
|
122
|
+
capabilities: {
|
|
123
|
+
sessionRunContent: true,
|
|
124
|
+
sessionRunFiles: true,
|
|
125
|
+
...(this.services.sessionTimeline
|
|
126
|
+
? { sessionTimelineProjectText: true }
|
|
127
|
+
: {}),
|
|
128
|
+
...(this.services.sessionBrowserRequestHeaders
|
|
129
|
+
? { sessionBrowserRequestHeaders: true }
|
|
130
|
+
: {}),
|
|
131
|
+
...(this.services.sessionPortal
|
|
132
|
+
? {
|
|
133
|
+
sessionPortalEmbedOrigin: true,
|
|
134
|
+
sessionPortalGrantRevocation: true,
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
137
|
+
},
|
|
138
|
+
...(this.services.sessionPortal
|
|
139
|
+
? { sessionPortal: { ...this.services.sessionPortal } }
|
|
140
|
+
: {}),
|
|
93
141
|
};
|
|
94
142
|
}
|
|
95
|
-
const capability = METHOD_CAPABILITY[method];
|
|
96
|
-
if (!capability)
|
|
97
|
-
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
98
|
-
if (!this.capabilities.has(capability)) {
|
|
99
|
-
throw new PluginHostCallError("CAPABILITY_NOT_DECLARED", `host method ${method} is missing manifest capability ${capability}`);
|
|
100
|
-
}
|
|
101
143
|
try {
|
|
102
144
|
switch (method) {
|
|
103
145
|
case "installation.proof.sign": {
|
|
@@ -118,8 +160,8 @@ export class PluginHostRpc {
|
|
|
118
160
|
return await this.ensureSession(params);
|
|
119
161
|
case "session.run":
|
|
120
162
|
return await this.runSession(params, signal);
|
|
121
|
-
case "session.
|
|
122
|
-
return this.
|
|
163
|
+
case "session.activity.get":
|
|
164
|
+
return await this.sessionActivity(params);
|
|
123
165
|
case "session.interrupt":
|
|
124
166
|
return await this.interruptRun(params);
|
|
125
167
|
case "session.interaction.resolve":
|
|
@@ -149,8 +191,31 @@ export class PluginHostRpc {
|
|
|
149
191
|
return this.services.sessionRegistry.list().filter((session) => this.isOwned(session));
|
|
150
192
|
case "session.meta.setTitle":
|
|
151
193
|
return this.setSessionTitle(params);
|
|
194
|
+
case "session.browser.requestHeaders.get":
|
|
195
|
+
return await this.getSessionBrowserRequestHeaders(params);
|
|
196
|
+
case "session.browser.requestHeaders.set":
|
|
197
|
+
return await this.setSessionBrowserRequestHeaders(params);
|
|
152
198
|
case "control.link.issue":
|
|
153
199
|
return this.issueControlLink(params);
|
|
200
|
+
case "session.portal.plugin.issue": {
|
|
201
|
+
const input = record(params);
|
|
202
|
+
return this.issueSessionPortalLink("plugin_sessions", requiredSessionPortalAccess(input), undefined, optionalSessionPortalEmbedOrigin(input.embedOrigin), optionalString(input.grantHandle, "grantHandle", 128));
|
|
203
|
+
}
|
|
204
|
+
case "session.portal.session.issue": {
|
|
205
|
+
const input = record(params);
|
|
206
|
+
const sessionId = requiredSessionId(input);
|
|
207
|
+
this.ownedSession(sessionId);
|
|
208
|
+
return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId, optionalSessionPortalEmbedOrigin(input.embedOrigin), optionalString(input.grantHandle, "grantHandle", 128));
|
|
209
|
+
}
|
|
210
|
+
case "session.portal.grant.revoke": {
|
|
211
|
+
const grantHandle = requiredString(record(params).grantHandle, "grantHandle", 128);
|
|
212
|
+
return revokeSessionPortalGrant({
|
|
213
|
+
pluginId: this.principalId,
|
|
214
|
+
grantHandle,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
case "session.timeline.projectText":
|
|
218
|
+
return await this.projectTimelineText(params);
|
|
154
219
|
default:
|
|
155
220
|
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
156
221
|
}
|
|
@@ -319,17 +384,86 @@ export class PluginHostRpc {
|
|
|
319
384
|
const input = record(params);
|
|
320
385
|
const sessionId = requiredSessionId(input);
|
|
321
386
|
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
387
|
+
const hasMessage = input.message !== undefined;
|
|
388
|
+
const hasContent = input.content !== undefined;
|
|
389
|
+
if (hasMessage === hasContent) {
|
|
390
|
+
throw new PluginHostCallError("INVALID_PARAMS", "provide exactly one of message or content");
|
|
325
391
|
}
|
|
326
|
-
|
|
327
|
-
|
|
392
|
+
const message = hasMessage
|
|
393
|
+
? requiredText(input.message, "message", MAX_MESSAGE_LENGTH)
|
|
394
|
+
: undefined;
|
|
395
|
+
const collaborationMode = optionalCollaborationMode(input.collaborationMode);
|
|
396
|
+
let session = this.ownedSession(sessionId);
|
|
397
|
+
if (operationId) {
|
|
398
|
+
const replayed = this.activeRunsByOperation.get(operationId);
|
|
399
|
+
if (replayed) {
|
|
400
|
+
if (replayed.sessionId !== sessionId) {
|
|
401
|
+
throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already belongs to another active Session: ${operationId}`, true);
|
|
402
|
+
}
|
|
403
|
+
return await waitForHostPromise(replayed.started, signal);
|
|
404
|
+
}
|
|
328
405
|
}
|
|
406
|
+
const active = this.activeSessionRuns.get(sessionId);
|
|
407
|
+
if (active) {
|
|
408
|
+
if (hasContent) {
|
|
409
|
+
// Native steering currently accepts text only. Preserve the attachment
|
|
410
|
+
// as a new Turn after the active response reaches its terminal edge.
|
|
411
|
+
await waitForHostPromise(active.settled, signal);
|
|
412
|
+
throwIfHostCallAborted(signal);
|
|
413
|
+
return await this.runSessionAdmitted(params, signal);
|
|
414
|
+
}
|
|
415
|
+
const queued = active.input.enqueue(message);
|
|
416
|
+
if (!queued.accepted) {
|
|
417
|
+
// The response reached its terminal boundary before Host cleanup won
|
|
418
|
+
// the scheduling race. Wait for that old Run to leave the Session slot,
|
|
419
|
+
// then submit this message as a new Turn with a new response identity.
|
|
420
|
+
await waitForHostPromise(active.settled, signal);
|
|
421
|
+
throwIfHostCallAborted(signal);
|
|
422
|
+
return await this.runSessionAdmitted(params, signal);
|
|
423
|
+
}
|
|
424
|
+
if (operationId) {
|
|
425
|
+
active.operationIds.add(operationId);
|
|
426
|
+
this.activeRunsByOperation.set(operationId, active);
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
// Reuse the current Run identity. Core already owns its response stream;
|
|
430
|
+
// the buffered input will become turn/steer once that stream subscribes.
|
|
431
|
+
return await waitForHostPromise(active.started, signal);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
queued.cancel();
|
|
435
|
+
if (operationId && this.activeRunsByOperation.get(operationId) === active) {
|
|
436
|
+
this.activeRunsByOperation.delete(operationId);
|
|
437
|
+
active.operationIds.delete(operationId);
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const modelProvided = input.model !== undefined;
|
|
443
|
+
const requestedModel = input.model === null || input.model === ""
|
|
444
|
+
? null
|
|
445
|
+
: optionalString(input.model, "model", 256);
|
|
446
|
+
const reasoningEffortProvided = input.reasoningEffort !== undefined;
|
|
447
|
+
const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
|
|
448
|
+
? null
|
|
449
|
+
: optionalString(input.reasoningEffort, "reasoningEffort", 64);
|
|
329
450
|
if (this.activeRuns.size >= PLUGIN_HOST_MAX_ACTIVE_RUNS) {
|
|
330
451
|
throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
|
|
331
452
|
}
|
|
332
|
-
const
|
|
453
|
+
const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
|
|
454
|
+
const nextReasoningEffort = reasoningEffortProvided
|
|
455
|
+
? requestedReasoningEffort ?? null
|
|
456
|
+
: session.execution.reasoningEffort;
|
|
457
|
+
if (nextModel !== session.execution.model ||
|
|
458
|
+
nextReasoningEffort !== session.execution.reasoningEffort) {
|
|
459
|
+
await this.services.sessionExecution.update({
|
|
460
|
+
sessionId,
|
|
461
|
+
model: nextModel,
|
|
462
|
+
reasoningEffort: nextReasoningEffort,
|
|
463
|
+
});
|
|
464
|
+
session = this.ownedSession(sessionId);
|
|
465
|
+
}
|
|
466
|
+
const content = hasContent ? await pluginRunContent(input.content) : undefined;
|
|
333
467
|
const abort = new AbortController();
|
|
334
468
|
const cancel = () => abort.abort(signal?.reason);
|
|
335
469
|
if (signal?.aborted) {
|
|
@@ -337,15 +471,35 @@ export class PluginHostRpc {
|
|
|
337
471
|
throwIfHostCallAborted(signal);
|
|
338
472
|
}
|
|
339
473
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
340
|
-
const
|
|
474
|
+
const runInput = new PluginRunInputController();
|
|
341
475
|
const runId = randomUUID();
|
|
476
|
+
let resolveStarted;
|
|
477
|
+
let rejectStarted;
|
|
478
|
+
const startedResult = new Promise((resolve, reject) => {
|
|
479
|
+
resolveStarted = resolve;
|
|
480
|
+
rejectStarted = reject;
|
|
481
|
+
});
|
|
482
|
+
// The initiating RPC observes startup directly below. This promise exists
|
|
483
|
+
// for concurrent/replayed calls, so retain its rejection without creating
|
|
484
|
+
// an unhandled-rejection when there are none.
|
|
485
|
+
void startedResult.catch(() => undefined);
|
|
486
|
+
let resolveSettled;
|
|
487
|
+
const settled = new Promise((resolve) => {
|
|
488
|
+
resolveSettled = resolve;
|
|
489
|
+
});
|
|
342
490
|
const run = {
|
|
343
491
|
runId,
|
|
344
492
|
sessionId,
|
|
345
|
-
operationId,
|
|
493
|
+
operationIds: new Set(operationId ? [operationId] : []),
|
|
346
494
|
interruptRequested: false,
|
|
347
495
|
abort,
|
|
348
|
-
|
|
496
|
+
input: runInput,
|
|
497
|
+
started: startedResult,
|
|
498
|
+
startupSettled: false,
|
|
499
|
+
resolveStarted,
|
|
500
|
+
rejectStarted,
|
|
501
|
+
settled,
|
|
502
|
+
resolveSettled,
|
|
349
503
|
};
|
|
350
504
|
this.activeRuns.set(runId, run);
|
|
351
505
|
this.activeSessionRuns.set(sessionId, run);
|
|
@@ -353,16 +507,23 @@ export class PluginHostRpc {
|
|
|
353
507
|
this.activeRunsByOperation.set(operationId, run);
|
|
354
508
|
let started;
|
|
355
509
|
try {
|
|
356
|
-
started = await this.services.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
510
|
+
started = await this.services.sessionRuns.startRun(sessionId, message !== undefined
|
|
511
|
+
? {
|
|
512
|
+
message,
|
|
513
|
+
clientMessageId: operationId ?? `plugin_run_${runId}`,
|
|
514
|
+
}
|
|
515
|
+
: {
|
|
516
|
+
content: content,
|
|
517
|
+
clientMessageId: operationId ?? `plugin_run_${runId}`,
|
|
518
|
+
}, {
|
|
361
519
|
signal: abort.signal,
|
|
362
|
-
steer,
|
|
520
|
+
steer: runInput,
|
|
521
|
+
...(collaborationMode ? { collaborationMode } : {}),
|
|
363
522
|
});
|
|
364
523
|
}
|
|
365
524
|
catch (error) {
|
|
525
|
+
run.startupSettled = true;
|
|
526
|
+
run.rejectStarted(error);
|
|
366
527
|
this.releaseRun(run);
|
|
367
528
|
throw error;
|
|
368
529
|
}
|
|
@@ -371,22 +532,33 @@ export class PluginHostRpc {
|
|
|
371
532
|
}
|
|
372
533
|
if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
|
|
373
534
|
abort.abort(signal?.reason);
|
|
535
|
+
const error = signal?.aborted
|
|
536
|
+
? new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled")
|
|
537
|
+
: new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
|
|
538
|
+
run.startupSettled = true;
|
|
539
|
+
run.rejectStarted(error);
|
|
374
540
|
this.releaseRun(run);
|
|
375
541
|
throwIfHostCallAborted(signal);
|
|
376
|
-
throw
|
|
542
|
+
throw error;
|
|
377
543
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
381
|
-
return {
|
|
544
|
+
run.responseId = started.responseId;
|
|
545
|
+
const result = {
|
|
382
546
|
runId,
|
|
547
|
+
...(started.responseId ? { responseId: started.responseId } : {}),
|
|
383
548
|
sessionId,
|
|
384
549
|
provider: started.provider,
|
|
385
550
|
model: started.model,
|
|
386
551
|
authSource: started.authSource,
|
|
387
552
|
};
|
|
553
|
+
run.startupSettled = true;
|
|
554
|
+
run.resolveStarted(result);
|
|
555
|
+
// Let the RPC transport write the request response before stream events are
|
|
556
|
+
// emitted. `runLive` has already subscribed, so no agent event is lost.
|
|
557
|
+
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
558
|
+
return result;
|
|
388
559
|
}
|
|
389
560
|
async pumpRun(run, generator) {
|
|
561
|
+
let terminalCompleted = false;
|
|
390
562
|
let terminalFailure;
|
|
391
563
|
try {
|
|
392
564
|
for await (const event of generator) {
|
|
@@ -396,6 +568,7 @@ export class PluginHostRpc {
|
|
|
396
568
|
return;
|
|
397
569
|
if (this.disposed || this.activeRuns.get(run.runId) !== run)
|
|
398
570
|
return;
|
|
571
|
+
run.responseId ??= sessionEventResponseId(event);
|
|
399
572
|
const finalResponse = topLevelAssistantResponse(event);
|
|
400
573
|
if (finalResponse !== undefined) {
|
|
401
574
|
run.finalAssistantResponse = truncateUtf8(finalResponse, MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES);
|
|
@@ -403,9 +576,23 @@ export class PluginHostRpc {
|
|
|
403
576
|
for (const projected of projectSessionEvent(run, event)) {
|
|
404
577
|
await this.emit(projected);
|
|
405
578
|
}
|
|
579
|
+
try {
|
|
580
|
+
const fileEvent = await this.projectSessionFileEvent(run, event);
|
|
581
|
+
if (fileEvent)
|
|
582
|
+
await this.emit(fileEvent);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// File delivery is an optional projection of a canonical event. A
|
|
586
|
+
// missing borrowed path must not turn an otherwise successful native
|
|
587
|
+
// Turn into session.run.failed or stop consuming its event stream.
|
|
588
|
+
}
|
|
406
589
|
terminalFailure = sessionRunFailure(event);
|
|
407
590
|
if (terminalFailure)
|
|
408
591
|
break;
|
|
592
|
+
if (sessionRunCompleted(event)) {
|
|
593
|
+
terminalCompleted = true;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
409
596
|
}
|
|
410
597
|
if (!this.disposed &&
|
|
411
598
|
this.activeRuns.get(run.runId) === run &&
|
|
@@ -420,7 +607,7 @@ export class PluginHostRpc {
|
|
|
420
607
|
},
|
|
421
608
|
});
|
|
422
609
|
}
|
|
423
|
-
else {
|
|
610
|
+
else if (terminalCompleted) {
|
|
424
611
|
await this.emit({
|
|
425
612
|
event: "session.run.completed",
|
|
426
613
|
payload: {
|
|
@@ -432,6 +619,20 @@ export class PluginHostRpc {
|
|
|
432
619
|
},
|
|
433
620
|
});
|
|
434
621
|
}
|
|
622
|
+
else {
|
|
623
|
+
await this.emit({
|
|
624
|
+
event: "session.run.failed",
|
|
625
|
+
payload: {
|
|
626
|
+
runId: run.runId,
|
|
627
|
+
sessionId: run.sessionId,
|
|
628
|
+
error: {
|
|
629
|
+
code: "RUNTIME_EVENT_STREAM_ENDED",
|
|
630
|
+
message: "Runtime event stream ended before a terminal response event",
|
|
631
|
+
retryable: true,
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
435
636
|
}
|
|
436
637
|
}
|
|
437
638
|
catch (error) {
|
|
@@ -465,28 +666,86 @@ export class PluginHostRpc {
|
|
|
465
666
|
this.releaseRun(run);
|
|
466
667
|
}
|
|
467
668
|
}
|
|
669
|
+
async projectSessionFileEvent(run, event) {
|
|
670
|
+
const input = jsonRecord(event);
|
|
671
|
+
if (input?.type !== "response.output_item.done")
|
|
672
|
+
return undefined;
|
|
673
|
+
const item = jsonRecord(input.item);
|
|
674
|
+
if (item?.type !== "function_call_output")
|
|
675
|
+
return undefined;
|
|
676
|
+
const data = jsonRecord(item.data);
|
|
677
|
+
const image = jsonRecord(data?.generatedImage);
|
|
678
|
+
if (!image || typeof image.mediaType !== "string")
|
|
679
|
+
return undefined;
|
|
680
|
+
const responseId = typeof item.responseId === "string"
|
|
681
|
+
? item.responseId
|
|
682
|
+
: run.responseId;
|
|
683
|
+
if (!responseId)
|
|
684
|
+
return undefined;
|
|
685
|
+
let filePath;
|
|
686
|
+
if (typeof image.path === "string" && path.isAbsolute(image.path)) {
|
|
687
|
+
filePath = image.path;
|
|
688
|
+
}
|
|
689
|
+
else if (typeof image.resourceId === "string") {
|
|
690
|
+
filePath = await this.services.sessionFiles.resolveResourcePath(run.sessionId, image.resourceId);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
event: "session.run.file",
|
|
697
|
+
payload: {
|
|
698
|
+
runId: run.runId,
|
|
699
|
+
responseId,
|
|
700
|
+
sessionId: run.sessionId,
|
|
701
|
+
itemId: typeof item.id === "string" ? item.id : "unknown",
|
|
702
|
+
filename: typeof image.filename === "string" && image.filename
|
|
703
|
+
? image.filename
|
|
704
|
+
: path.basename(filePath),
|
|
705
|
+
mediaType: image.mediaType,
|
|
706
|
+
path: filePath,
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
468
710
|
releaseRun(run) {
|
|
711
|
+
run.input.close();
|
|
712
|
+
if (!run.startupSettled) {
|
|
713
|
+
run.startupSettled = true;
|
|
714
|
+
run.rejectStarted(new PluginHostCallError("HOST_CLOSED", "plugin Host closed the active Run"));
|
|
715
|
+
}
|
|
469
716
|
if (this.activeRuns.get(run.runId) === run)
|
|
470
717
|
this.activeRuns.delete(run.runId);
|
|
471
718
|
if (this.activeSessionRuns.get(run.sessionId) === run) {
|
|
472
719
|
this.activeSessionRuns.delete(run.sessionId);
|
|
473
720
|
}
|
|
474
|
-
|
|
475
|
-
this.activeRunsByOperation.
|
|
721
|
+
for (const operationId of run.operationIds) {
|
|
722
|
+
if (this.activeRunsByOperation.get(operationId) === run) {
|
|
723
|
+
this.activeRunsByOperation.delete(operationId);
|
|
724
|
+
}
|
|
476
725
|
}
|
|
477
|
-
|
|
478
|
-
steerSession(params) {
|
|
479
|
-
const input = record(params);
|
|
480
|
-
const run = this.activeRun(input);
|
|
481
|
-
const text = requiredText(input.text, "text", MAX_MESSAGE_LENGTH);
|
|
482
|
-
return { delivered: run.steer.send(text) };
|
|
726
|
+
run.resolveSettled();
|
|
483
727
|
}
|
|
484
728
|
async interruptRun(params) {
|
|
485
729
|
const input = record(params);
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
730
|
+
const sessionId = optionalString(input.sessionId, "sessionId", 128);
|
|
731
|
+
const runId = optionalString(input.runId, "runId", 128);
|
|
732
|
+
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
733
|
+
if ((sessionId ? 1 : 0) + (runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
|
|
734
|
+
throw new PluginHostCallError("INVALID_PARAMS", "exactly one of sessionId, runId, or operationId is required");
|
|
735
|
+
}
|
|
736
|
+
let run;
|
|
737
|
+
if (sessionId) {
|
|
738
|
+
this.ownedSession(sessionId);
|
|
739
|
+
run = this.activeSessionRuns.get(sessionId);
|
|
740
|
+
if (!run) {
|
|
741
|
+
const interrupted = this.services.interruptSession
|
|
742
|
+
? Promise.resolve().then(() => this.services.interruptSession(sessionId))
|
|
743
|
+
: Promise.resolve(false);
|
|
744
|
+
return { interrupted: await interrupted };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
run = this.activeRunByIdentity(input);
|
|
490
749
|
}
|
|
491
750
|
if (run.interruptResult) {
|
|
492
751
|
return { interrupted: await run.interruptResult };
|
|
@@ -506,9 +765,7 @@ export class PluginHostRpc {
|
|
|
506
765
|
// effort and cannot prove that native execution stopped.
|
|
507
766
|
return false;
|
|
508
767
|
});
|
|
509
|
-
const interruptResult =
|
|
510
|
-
? graceful
|
|
511
|
-
: forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
|
|
768
|
+
const interruptResult = graceful;
|
|
512
769
|
run.interruptResult = interruptResult;
|
|
513
770
|
try {
|
|
514
771
|
const interrupted = await interruptResult;
|
|
@@ -526,6 +783,17 @@ export class PluginHostRpc {
|
|
|
526
783
|
throw error;
|
|
527
784
|
}
|
|
528
785
|
}
|
|
786
|
+
async sessionActivity(params) {
|
|
787
|
+
const sessionId = requiredSessionId(params);
|
|
788
|
+
this.ownedSession(sessionId);
|
|
789
|
+
const snapshot = structuredClone(await this.services.sessionActivity(sessionId));
|
|
790
|
+
const waiting = snapshot.pendingInteractions.length > 0;
|
|
791
|
+
return {
|
|
792
|
+
status: waiting ? "waiting" : snapshot.status,
|
|
793
|
+
activeResponseIds: snapshot.activeResponseIds,
|
|
794
|
+
pendingInteractions: snapshot.pendingInteractions,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
529
797
|
async interruptWon(run) {
|
|
530
798
|
if (!run.interruptRequested)
|
|
531
799
|
return false;
|
|
@@ -533,13 +801,6 @@ export class PluginHostRpc {
|
|
|
533
801
|
return true;
|
|
534
802
|
return run.interruptResult.catch(() => false);
|
|
535
803
|
}
|
|
536
|
-
activeRun(input) {
|
|
537
|
-
const runId = requiredString(input.runId, "runId", 128);
|
|
538
|
-
const run = this.activeRuns.get(runId);
|
|
539
|
-
if (!run)
|
|
540
|
-
throw new PluginHostCallError("RUN_NOT_FOUND", `active run not found: ${runId}`);
|
|
541
|
-
return run;
|
|
542
|
-
}
|
|
543
804
|
activeRunByIdentity(input) {
|
|
544
805
|
const runId = optionalString(input.runId, "runId", 128);
|
|
545
806
|
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
@@ -633,6 +894,36 @@ export class PluginHostRpc {
|
|
|
633
894
|
this.services.sessionRegistry.setTitle(sessionId, title);
|
|
634
895
|
return { updated: true };
|
|
635
896
|
}
|
|
897
|
+
async getSessionBrowserRequestHeaders(params) {
|
|
898
|
+
let input;
|
|
899
|
+
try {
|
|
900
|
+
input = parseRuntimeBrowserRequestHeadersGetParams(params);
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
|
|
904
|
+
}
|
|
905
|
+
this.ownedSession(input.sessionId);
|
|
906
|
+
const service = this.services.sessionBrowserRequestHeaders;
|
|
907
|
+
if (!service) {
|
|
908
|
+
throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
|
|
909
|
+
}
|
|
910
|
+
return await service.getRequestHeaders(input);
|
|
911
|
+
}
|
|
912
|
+
async setSessionBrowserRequestHeaders(params) {
|
|
913
|
+
let input;
|
|
914
|
+
try {
|
|
915
|
+
input = parseRuntimeBrowserRequestHeadersSetParams(params);
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
throw new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
|
|
919
|
+
}
|
|
920
|
+
this.ownedSession(input.sessionId);
|
|
921
|
+
const service = this.services.sessionBrowserRequestHeaders;
|
|
922
|
+
if (!service) {
|
|
923
|
+
throw new PluginHostCallError("METHOD_NOT_FOUND", "Session Browser request headers are unsupported by this Host");
|
|
924
|
+
}
|
|
925
|
+
return await service.setRequestHeaders(input);
|
|
926
|
+
}
|
|
636
927
|
resolveAgentExecution(params) {
|
|
637
928
|
const input = record(params);
|
|
638
929
|
const defaultAgent = optionalAgentId(input.defaultAgent, "defaultAgent");
|
|
@@ -652,6 +943,56 @@ export class PluginHostRpc {
|
|
|
652
943
|
...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
|
|
653
944
|
});
|
|
654
945
|
}
|
|
946
|
+
issueSessionPortalLink(scope, access, sessionId, embedOrigin, grantHandle) {
|
|
947
|
+
if (!this.services.sessionPortal) {
|
|
948
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
|
|
949
|
+
}
|
|
950
|
+
return issueSessionPortalTicket({
|
|
951
|
+
pluginId: this.principalId,
|
|
952
|
+
scope,
|
|
953
|
+
access,
|
|
954
|
+
...(sessionId ? { sessionId } : {}),
|
|
955
|
+
...(embedOrigin ? { embedOrigin } : {}),
|
|
956
|
+
...(grantHandle ? { grantHandle } : {}),
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
async projectTimelineText(params) {
|
|
960
|
+
const sessionTimeline = this.services.sessionTimeline;
|
|
961
|
+
if (!sessionTimeline) {
|
|
962
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "Session timeline projection is unavailable");
|
|
963
|
+
}
|
|
964
|
+
const input = record(params);
|
|
965
|
+
const sessionId = requiredSessionId(input);
|
|
966
|
+
this.ownedSession(sessionId);
|
|
967
|
+
const projection = claimSessionTimelineProjection({
|
|
968
|
+
pluginId: this.principalId,
|
|
969
|
+
sessionId,
|
|
970
|
+
projectionId: requiredString(input.projectionId, "projectionId", 256),
|
|
971
|
+
...(input.responseId === undefined
|
|
972
|
+
? {}
|
|
973
|
+
: { responseId: requiredString(input.responseId, "responseId", 256) }),
|
|
974
|
+
text: requiredText(input.text, "text", 64 * 1024),
|
|
975
|
+
});
|
|
976
|
+
if (!(await sessionTimeline.hasItem(sessionId, projection.itemId))) {
|
|
977
|
+
await sessionTimeline.projectText({
|
|
978
|
+
sessionId,
|
|
979
|
+
responseId: projection.responseId,
|
|
980
|
+
itemId: projection.itemId,
|
|
981
|
+
text: projection.text,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
completeSessionTimelineProjection({
|
|
985
|
+
pluginId: this.principalId,
|
|
986
|
+
sessionId,
|
|
987
|
+
projectionId: projection.projectionId,
|
|
988
|
+
});
|
|
989
|
+
return {
|
|
990
|
+
projectionId: projection.projectionId,
|
|
991
|
+
responseId: projection.responseId,
|
|
992
|
+
itemId: projection.itemId,
|
|
993
|
+
disposition: projection.disposition,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
655
996
|
ownedSession(sessionId) {
|
|
656
997
|
const session = this.services.sessionRegistry.get(sessionId);
|
|
657
998
|
if (!session || !this.isOwned(session)) {
|
|
@@ -678,6 +1019,24 @@ function throwIfHostCallAborted(signal) {
|
|
|
678
1019
|
return;
|
|
679
1020
|
throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
|
|
680
1021
|
}
|
|
1022
|
+
async function waitForHostPromise(promise, signal) {
|
|
1023
|
+
throwIfHostCallAborted(signal);
|
|
1024
|
+
if (!signal)
|
|
1025
|
+
return await promise;
|
|
1026
|
+
return await new Promise((resolve, reject) => {
|
|
1027
|
+
const aborted = () => {
|
|
1028
|
+
reject(new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled"));
|
|
1029
|
+
};
|
|
1030
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
1031
|
+
void promise.then((value) => {
|
|
1032
|
+
signal.removeEventListener("abort", aborted);
|
|
1033
|
+
resolve(value);
|
|
1034
|
+
}, (error) => {
|
|
1035
|
+
signal.removeEventListener("abort", aborted);
|
|
1036
|
+
reject(error);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
681
1040
|
export class PluginHostCallError extends PluginRunnerError {
|
|
682
1041
|
retryable;
|
|
683
1042
|
data;
|
|
@@ -694,12 +1053,30 @@ export class PluginHostCallError extends PluginRunnerError {
|
|
|
694
1053
|
function toHostCallError(error) {
|
|
695
1054
|
if (error instanceof PluginHostCallError)
|
|
696
1055
|
return error;
|
|
1056
|
+
if (error instanceof SessionBrowserServiceError) {
|
|
1057
|
+
if (error.code === "revision_mismatch") {
|
|
1058
|
+
return new PluginHostCallError("REVISION_CONFLICT", "Session Browser request headers changed since they were read");
|
|
1059
|
+
}
|
|
1060
|
+
if (error.code === "invalid_request") {
|
|
1061
|
+
return new PluginHostCallError("INVALID_PARAMS", "Session Browser request headers parameters are invalid");
|
|
1062
|
+
}
|
|
1063
|
+
if (error.code === "not_found") {
|
|
1064
|
+
return new PluginHostCallError("SESSION_NOT_FOUND", "plugin session not found");
|
|
1065
|
+
}
|
|
1066
|
+
return new PluginHostCallError("HOST_CALL_FAILED", "Session Browser request headers operation failed");
|
|
1067
|
+
}
|
|
697
1068
|
if (error instanceof AgentRuntimeError && error.code === "live_unavailable") {
|
|
698
1069
|
// ConversationRuntime emits this code only before injectMessage is called:
|
|
699
1070
|
// the native live Session (including its exact Skill environment) could
|
|
700
1071
|
// not be brought up, so callers may fail without quarantining capacity.
|
|
701
1072
|
return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
|
|
702
1073
|
}
|
|
1074
|
+
if (error instanceof AgentRuntimeError) {
|
|
1075
|
+
// Native lifecycle failures already carry a stable phase code. Preserve it
|
|
1076
|
+
// across the plugin-host boundary instead of erasing it behind the generic
|
|
1077
|
+
// HOST_CALL_FAILED wrapper.
|
|
1078
|
+
return new PluginHostCallError(error.code, error.message, false);
|
|
1079
|
+
}
|
|
703
1080
|
if (error instanceof SessionLaunchOperationConflictError) {
|
|
704
1081
|
return new PluginHostCallError(error.code, error.message, false);
|
|
705
1082
|
}
|
|
@@ -717,7 +1094,10 @@ function sessionLaunchSelection(value) {
|
|
|
717
1094
|
const agent = optionalAgentId(input.agent, "session.agent");
|
|
718
1095
|
const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
|
|
719
1096
|
const provider = optionalRuntimeValue(input.provider);
|
|
720
|
-
const
|
|
1097
|
+
const modelProvided = input.model !== undefined;
|
|
1098
|
+
const model = input.model === null || input.model === ""
|
|
1099
|
+
? null
|
|
1100
|
+
: optionalString(input.model, "session.model", 256);
|
|
721
1101
|
const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
|
|
722
1102
|
const title = optionalString(input.title, "session.title", 1_000);
|
|
723
1103
|
if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
|
|
@@ -736,7 +1116,7 @@ function sessionLaunchSelection(value) {
|
|
|
736
1116
|
...(agent ? { agent } : {}),
|
|
737
1117
|
...(config ? { config } : {}),
|
|
738
1118
|
...(provider ? { provider } : {}),
|
|
739
|
-
...(
|
|
1119
|
+
...(modelProvided ? { model: model ?? null } : {}),
|
|
740
1120
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
741
1121
|
...(title ? { title } : {}),
|
|
742
1122
|
};
|
|
@@ -768,6 +1148,114 @@ function sessionExecutionSnapshot(value) {
|
|
|
768
1148
|
function requiredSessionId(params) {
|
|
769
1149
|
return requiredString(record(params).sessionId, "sessionId", 512);
|
|
770
1150
|
}
|
|
1151
|
+
async function pluginRunContent(value) {
|
|
1152
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
|
|
1153
|
+
throw new PluginHostCallError("INVALID_PARAMS", "content must be a non-empty array with at most 256 parts");
|
|
1154
|
+
}
|
|
1155
|
+
return Promise.all(value.map(async (entry, index) => {
|
|
1156
|
+
const input = record(entry);
|
|
1157
|
+
if (input.type === "input_text") {
|
|
1158
|
+
return {
|
|
1159
|
+
type: "input_text",
|
|
1160
|
+
text: requiredText(input.text, `content[${index}].text`, MAX_MESSAGE_LENGTH),
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (input.type !== "input_image" && input.type !== "input_file") {
|
|
1164
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].type is invalid`);
|
|
1165
|
+
}
|
|
1166
|
+
const suppliedPath = requiredPathString(input.path, `content[${index}].path`, 16_384);
|
|
1167
|
+
if (!path.isAbsolute(suppliedPath)) {
|
|
1168
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path must be absolute`);
|
|
1169
|
+
}
|
|
1170
|
+
let resolvedPath;
|
|
1171
|
+
let fileSize;
|
|
1172
|
+
try {
|
|
1173
|
+
resolvedPath = await realpath(suppliedPath);
|
|
1174
|
+
const metadata = await stat(resolvedPath);
|
|
1175
|
+
if (!metadata.isFile())
|
|
1176
|
+
throw new Error("not a regular file");
|
|
1177
|
+
fileSize = metadata.size;
|
|
1178
|
+
}
|
|
1179
|
+
catch (error) {
|
|
1180
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path is not a readable regular file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1181
|
+
}
|
|
1182
|
+
const filename = input.type === "input_file"
|
|
1183
|
+
? requiredString(input.filename, `content[${index}].filename`, 255)
|
|
1184
|
+
: optionalString(input.filename, `content[${index}].filename`, 255) ??
|
|
1185
|
+
path.basename(resolvedPath);
|
|
1186
|
+
if (input.type === "input_image") {
|
|
1187
|
+
return {
|
|
1188
|
+
type: "input_image",
|
|
1189
|
+
path: resolvedPath,
|
|
1190
|
+
filename,
|
|
1191
|
+
mediaType: pluginImageMediaType(input.mediaType, resolvedPath, `content[${index}].mediaType`),
|
|
1192
|
+
byteLength: fileSize,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
return {
|
|
1196
|
+
type: "input_file",
|
|
1197
|
+
path: resolvedPath,
|
|
1198
|
+
filename,
|
|
1199
|
+
mediaType: pluginMediaType(input.mediaType ?? "application/octet-stream", `content[${index}].mediaType`),
|
|
1200
|
+
byteLength: fileSize,
|
|
1201
|
+
};
|
|
1202
|
+
}));
|
|
1203
|
+
}
|
|
1204
|
+
function pluginImageMediaType(value, filePath, label) {
|
|
1205
|
+
const inferred = (() => {
|
|
1206
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
1207
|
+
case ".png": return "image/png";
|
|
1208
|
+
case ".jpg":
|
|
1209
|
+
case ".jpeg": return "image/jpeg";
|
|
1210
|
+
case ".webp": return "image/webp";
|
|
1211
|
+
case ".gif": return "image/gif";
|
|
1212
|
+
case ".svg": return "image/svg+xml";
|
|
1213
|
+
default: return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
})();
|
|
1216
|
+
const mediaType = value === undefined ? inferred : pluginMediaType(value, label);
|
|
1217
|
+
if (mediaType !== "image/png" &&
|
|
1218
|
+
mediaType !== "image/jpeg" &&
|
|
1219
|
+
mediaType !== "image/webp" &&
|
|
1220
|
+
mediaType !== "image/gif" &&
|
|
1221
|
+
mediaType !== "image/svg+xml") {
|
|
1222
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a supported image type`);
|
|
1223
|
+
}
|
|
1224
|
+
return mediaType;
|
|
1225
|
+
}
|
|
1226
|
+
function pluginMediaType(value, label) {
|
|
1227
|
+
if (typeof value !== "string") {
|
|
1228
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a media type`);
|
|
1229
|
+
}
|
|
1230
|
+
const mediaType = value.trim().toLowerCase();
|
|
1231
|
+
if (mediaType.length === 0 ||
|
|
1232
|
+
mediaType.length > 255 ||
|
|
1233
|
+
!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)) {
|
|
1234
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is invalid`);
|
|
1235
|
+
}
|
|
1236
|
+
return mediaType;
|
|
1237
|
+
}
|
|
1238
|
+
function requiredSessionPortalAccess(params) {
|
|
1239
|
+
const access = record(params).access;
|
|
1240
|
+
if (access === "read_only" || access === "read_write")
|
|
1241
|
+
return access;
|
|
1242
|
+
throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
|
|
1243
|
+
}
|
|
1244
|
+
function optionalSessionPortalEmbedOrigin(value) {
|
|
1245
|
+
if (value === undefined)
|
|
1246
|
+
return undefined;
|
|
1247
|
+
const candidate = requiredString(value, "embedOrigin", 512);
|
|
1248
|
+
try {
|
|
1249
|
+
const parsed = new URL(candidate);
|
|
1250
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
1251
|
+
parsed.origin !== candidate)
|
|
1252
|
+
throw new Error("invalid origin");
|
|
1253
|
+
return parsed.origin;
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
throw new PluginHostCallError("INVALID_PARAMS", "embedOrigin must be an exact http(s) origin");
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
771
1259
|
function sessionInteractionResolution(value) {
|
|
772
1260
|
const input = record(value);
|
|
773
1261
|
const keys = Object.keys(input);
|
|
@@ -810,6 +1298,13 @@ function sessionInteractionResolution(value) {
|
|
|
810
1298
|
function optionalRuntime(params) {
|
|
811
1299
|
return optionalRuntimeValue(record(params).runtime);
|
|
812
1300
|
}
|
|
1301
|
+
function optionalCollaborationMode(value) {
|
|
1302
|
+
if (value === undefined)
|
|
1303
|
+
return undefined;
|
|
1304
|
+
if (value === "plan" || value === "default")
|
|
1305
|
+
return value;
|
|
1306
|
+
throw new PluginHostCallError("INVALID_PARAMS", "collaborationMode must be plan or default");
|
|
1307
|
+
}
|
|
813
1308
|
function optionalRuntimeValue(value) {
|
|
814
1309
|
if (value === undefined || value === null || value === "")
|
|
815
1310
|
return undefined;
|
|
@@ -1024,8 +1519,22 @@ function topLevelAssistantResponse(event) {
|
|
|
1024
1519
|
.trim();
|
|
1025
1520
|
return text || undefined;
|
|
1026
1521
|
}
|
|
1522
|
+
function sessionEventResponseId(event) {
|
|
1523
|
+
const input = jsonRecord(event);
|
|
1524
|
+
if (typeof input?.responseId === "string")
|
|
1525
|
+
return input.responseId;
|
|
1526
|
+
const item = jsonRecord(input?.item);
|
|
1527
|
+
return typeof item?.responseId === "string" ? item.responseId : undefined;
|
|
1528
|
+
}
|
|
1027
1529
|
function sessionRunFailure(event) {
|
|
1028
1530
|
const input = jsonRecord(event);
|
|
1531
|
+
if (input?.type === "session.interrupted") {
|
|
1532
|
+
return {
|
|
1533
|
+
code: "RUNTIME_TURN_INTERRUPTED",
|
|
1534
|
+
message: "Runtime Turn was interrupted",
|
|
1535
|
+
retryable: false,
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1029
1538
|
if (input?.type !== "response.failed")
|
|
1030
1539
|
return undefined;
|
|
1031
1540
|
const error = jsonRecord(input.error);
|
|
@@ -1041,6 +1550,9 @@ function sessionRunFailure(event) {
|
|
|
1041
1550
|
retryable: false,
|
|
1042
1551
|
};
|
|
1043
1552
|
}
|
|
1553
|
+
function sessionRunCompleted(event) {
|
|
1554
|
+
return jsonRecord(event)?.type === "response.completed";
|
|
1555
|
+
}
|
|
1044
1556
|
function sessionHostEvent(run, event, delivery) {
|
|
1045
1557
|
return {
|
|
1046
1558
|
event: "session.run.event",
|