@rynx-ai/daemon 0.1.11-beta.3 → 0.1.11-beta.30
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 +6046 -6018
- package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5987
- 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-runtime.js +4 -3
- package/dist/chrome-for-testing-store.js +29 -7
- 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 +25 -14
- package/dist/db.d.ts +2 -2
- package/dist/db.js +133 -21
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +3 -0
- 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 +31 -9
- package/dist/plugin-host-rpc.js +428 -80
- 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/remote-runtime-access-store.d.ts +2 -2
- package/dist/remote-runtime-access-store.js +6 -9
- package/dist/remote-runtime-connection-manager.js +1 -1
- 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 +82 -28
- package/dist/session-launch-operations.js +6 -5
- package/dist/session-log-store.d.ts +1 -0
- package/dist/session-log-store.js +19 -16
- 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 +29 -0
- package/dist/session-portal-grant-store.js +85 -0
- package/dist/session-resource-store.d.ts +20 -6
- package/dist/session-resource-store.js +364 -68
- 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/system-dependencies.d.ts +22 -0
- package/dist/system-dependencies.js +79 -0
- package/package.json +15 -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,12 @@
|
|
|
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
6
|
import { PluginRunnerError } from "@rynx-ai/plugin-runner";
|
|
7
7
|
import { ensurePluginSessionLaunch, replayPluginSessionLaunch, SessionLaunchOperationConflictError, SessionLaunchOperationTargetDeletedError, } from "./session-launch-operations.js";
|
|
8
8
|
import { createControlLink } from "./control-link-store.js";
|
|
9
|
+
import { issueSessionPortalTicket } from "./session-portal-grant-store.js";
|
|
9
10
|
const MAX_MESSAGE_LENGTH = 1_000_000;
|
|
10
11
|
/** Leaves ample room for the RPC envelope below the runner's 1 MiB line cap. */
|
|
11
12
|
export const MAX_PLUGIN_HOST_EVENT_BYTES = 512 * 1024;
|
|
@@ -13,31 +14,59 @@ const MAX_PLUGIN_RUN_FINAL_RESPONSE_BYTES = 64 * 1024;
|
|
|
13
14
|
const MAX_DELTA_CHUNK_CODE_UNITS = 64 * 1024;
|
|
14
15
|
const SESSION_INTERACTION_RESOLUTION_MAX_BYTES = 128 * 1024;
|
|
15
16
|
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
17
|
/**
|
|
40
|
-
*
|
|
18
|
+
* Buffers messages that arrive while the first `session.run` is still bringing
|
|
19
|
+
* up its native Turn. Once Core subscribes, later messages are delivered
|
|
20
|
+
* through the same input channel. After that subscription closes, this Run no
|
|
21
|
+
* longer accepts input; the caller waits for release and opens the next Run.
|
|
22
|
+
*/
|
|
23
|
+
class PluginRunInputController {
|
|
24
|
+
listener;
|
|
25
|
+
pending = new Map();
|
|
26
|
+
subscribed = false;
|
|
27
|
+
closed = false;
|
|
28
|
+
enqueue(text) {
|
|
29
|
+
if (this.closed || (this.subscribed && !this.listener)) {
|
|
30
|
+
return { accepted: false, cancel: () => undefined };
|
|
31
|
+
}
|
|
32
|
+
if (this.listener) {
|
|
33
|
+
this.listener(text);
|
|
34
|
+
return { accepted: true, cancel: () => undefined };
|
|
35
|
+
}
|
|
36
|
+
const token = Symbol("plugin-run-input");
|
|
37
|
+
this.pending.set(token, text);
|
|
38
|
+
return {
|
|
39
|
+
accepted: true,
|
|
40
|
+
cancel: () => {
|
|
41
|
+
this.pending.delete(token);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
subscribe(listener) {
|
|
46
|
+
if (this.closed)
|
|
47
|
+
return () => undefined;
|
|
48
|
+
if (this.listener) {
|
|
49
|
+
throw new Error("plugin Run input already has a subscriber");
|
|
50
|
+
}
|
|
51
|
+
this.subscribed = true;
|
|
52
|
+
this.listener = listener;
|
|
53
|
+
const queued = [...this.pending.values()];
|
|
54
|
+
this.pending.clear();
|
|
55
|
+
for (const text of queued)
|
|
56
|
+
listener(text);
|
|
57
|
+
return () => {
|
|
58
|
+
if (this.listener === listener)
|
|
59
|
+
this.listener = undefined;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
close() {
|
|
63
|
+
this.closed = true;
|
|
64
|
+
this.listener = undefined;
|
|
65
|
+
this.pending.clear();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One authenticated plugin's Host RPC surface.
|
|
41
70
|
*
|
|
42
71
|
* This adapter is deliberately the only place that maps public string methods
|
|
43
72
|
* to trusted runtime objects. The runner receives neither this class nor its
|
|
@@ -49,7 +78,6 @@ export class PluginHostRpc {
|
|
|
49
78
|
daemonInstanceId;
|
|
50
79
|
productVersion;
|
|
51
80
|
cliVersion;
|
|
52
|
-
capabilities;
|
|
53
81
|
services;
|
|
54
82
|
managedWorkspaceRoot;
|
|
55
83
|
emit;
|
|
@@ -65,7 +93,6 @@ export class PluginHostRpc {
|
|
|
65
93
|
this.daemonInstanceId = requiredString(options.daemonInstanceId, "daemonInstanceId", 128);
|
|
66
94
|
this.productVersion = requiredString(options.productVersion, "productVersion", 128);
|
|
67
95
|
this.cliVersion = requiredString(options.cliVersion, "cliVersion", 128);
|
|
68
|
-
this.capabilities = new Set(options.capabilities);
|
|
69
96
|
this.services = options.services;
|
|
70
97
|
this.managedWorkspaceRoot = options.managedWorkspaceRoot
|
|
71
98
|
? path.resolve(options.managedWorkspaceRoot)
|
|
@@ -89,15 +116,15 @@ export class PluginHostRpc {
|
|
|
89
116
|
publicKey: this.services.installationProof.publicKey,
|
|
90
117
|
},
|
|
91
118
|
protocolVersion: 1,
|
|
92
|
-
capabilities:
|
|
119
|
+
capabilities: {
|
|
120
|
+
sessionRunContent: true,
|
|
121
|
+
sessionRunFiles: true,
|
|
122
|
+
},
|
|
123
|
+
...(this.services.sessionPortal
|
|
124
|
+
? { sessionPortal: { ...this.services.sessionPortal } }
|
|
125
|
+
: {}),
|
|
93
126
|
};
|
|
94
127
|
}
|
|
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
128
|
try {
|
|
102
129
|
switch (method) {
|
|
103
130
|
case "installation.proof.sign": {
|
|
@@ -118,8 +145,8 @@ export class PluginHostRpc {
|
|
|
118
145
|
return await this.ensureSession(params);
|
|
119
146
|
case "session.run":
|
|
120
147
|
return await this.runSession(params, signal);
|
|
121
|
-
case "session.
|
|
122
|
-
return this.
|
|
148
|
+
case "session.activity.get":
|
|
149
|
+
return await this.sessionActivity(params);
|
|
123
150
|
case "session.interrupt":
|
|
124
151
|
return await this.interruptRun(params);
|
|
125
152
|
case "session.interaction.resolve":
|
|
@@ -151,6 +178,16 @@ export class PluginHostRpc {
|
|
|
151
178
|
return this.setSessionTitle(params);
|
|
152
179
|
case "control.link.issue":
|
|
153
180
|
return this.issueControlLink(params);
|
|
181
|
+
case "session.portal.plugin.issue": {
|
|
182
|
+
const access = requiredSessionPortalAccess(params);
|
|
183
|
+
return this.issueSessionPortalLink("plugin_sessions", access);
|
|
184
|
+
}
|
|
185
|
+
case "session.portal.session.issue": {
|
|
186
|
+
const input = record(params);
|
|
187
|
+
const sessionId = requiredSessionId(input);
|
|
188
|
+
this.ownedSession(sessionId);
|
|
189
|
+
return this.issueSessionPortalLink("single_session", requiredSessionPortalAccess(input), sessionId);
|
|
190
|
+
}
|
|
154
191
|
default:
|
|
155
192
|
throw new PluginHostCallError("METHOD_NOT_FOUND", `unknown host method: ${method}`);
|
|
156
193
|
}
|
|
@@ -319,17 +356,86 @@ export class PluginHostRpc {
|
|
|
319
356
|
const input = record(params);
|
|
320
357
|
const sessionId = requiredSessionId(input);
|
|
321
358
|
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
359
|
+
const hasMessage = input.message !== undefined;
|
|
360
|
+
const hasContent = input.content !== undefined;
|
|
361
|
+
if (hasMessage === hasContent) {
|
|
362
|
+
throw new PluginHostCallError("INVALID_PARAMS", "provide exactly one of message or content");
|
|
363
|
+
}
|
|
364
|
+
const message = hasMessage
|
|
365
|
+
? requiredText(input.message, "message", MAX_MESSAGE_LENGTH)
|
|
366
|
+
: undefined;
|
|
367
|
+
const collaborationMode = optionalCollaborationMode(input.collaborationMode);
|
|
368
|
+
let session = this.ownedSession(sessionId);
|
|
369
|
+
if (operationId) {
|
|
370
|
+
const replayed = this.activeRunsByOperation.get(operationId);
|
|
371
|
+
if (replayed) {
|
|
372
|
+
if (replayed.sessionId !== sessionId) {
|
|
373
|
+
throw new PluginHostCallError("RUN_ALREADY_ACTIVE", `operation already belongs to another active Session: ${operationId}`, true);
|
|
374
|
+
}
|
|
375
|
+
return await waitForHostPromise(replayed.started, signal);
|
|
376
|
+
}
|
|
325
377
|
}
|
|
326
|
-
|
|
327
|
-
|
|
378
|
+
const active = this.activeSessionRuns.get(sessionId);
|
|
379
|
+
if (active) {
|
|
380
|
+
if (hasContent) {
|
|
381
|
+
// Native steering currently accepts text only. Preserve the attachment
|
|
382
|
+
// as a new Turn after the active response reaches its terminal edge.
|
|
383
|
+
await waitForHostPromise(active.settled, signal);
|
|
384
|
+
throwIfHostCallAborted(signal);
|
|
385
|
+
return await this.runSessionAdmitted(params, signal);
|
|
386
|
+
}
|
|
387
|
+
const queued = active.input.enqueue(message);
|
|
388
|
+
if (!queued.accepted) {
|
|
389
|
+
// The response reached its terminal boundary before Host cleanup won
|
|
390
|
+
// the scheduling race. Wait for that old Run to leave the Session slot,
|
|
391
|
+
// then submit this message as a new Turn with a new response identity.
|
|
392
|
+
await waitForHostPromise(active.settled, signal);
|
|
393
|
+
throwIfHostCallAborted(signal);
|
|
394
|
+
return await this.runSessionAdmitted(params, signal);
|
|
395
|
+
}
|
|
396
|
+
if (operationId) {
|
|
397
|
+
active.operationIds.add(operationId);
|
|
398
|
+
this.activeRunsByOperation.set(operationId, active);
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
// Reuse the current Run identity. Core already owns its response stream;
|
|
402
|
+
// the buffered input will become turn/steer once that stream subscribes.
|
|
403
|
+
return await waitForHostPromise(active.started, signal);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
queued.cancel();
|
|
407
|
+
if (operationId && this.activeRunsByOperation.get(operationId) === active) {
|
|
408
|
+
this.activeRunsByOperation.delete(operationId);
|
|
409
|
+
active.operationIds.delete(operationId);
|
|
410
|
+
}
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
328
413
|
}
|
|
414
|
+
const modelProvided = input.model !== undefined;
|
|
415
|
+
const requestedModel = input.model === null || input.model === ""
|
|
416
|
+
? null
|
|
417
|
+
: optionalString(input.model, "model", 256);
|
|
418
|
+
const reasoningEffortProvided = input.reasoningEffort !== undefined;
|
|
419
|
+
const requestedReasoningEffort = input.reasoningEffort === null || input.reasoningEffort === ""
|
|
420
|
+
? null
|
|
421
|
+
: optionalString(input.reasoningEffort, "reasoningEffort", 64);
|
|
329
422
|
if (this.activeRuns.size >= PLUGIN_HOST_MAX_ACTIVE_RUNS) {
|
|
330
423
|
throw new PluginHostCallError("RUN_LIMIT", "plugin has too many active session runs", true);
|
|
331
424
|
}
|
|
332
|
-
const
|
|
425
|
+
const nextModel = modelProvided ? requestedModel ?? null : session.execution.model;
|
|
426
|
+
const nextReasoningEffort = reasoningEffortProvided
|
|
427
|
+
? requestedReasoningEffort ?? null
|
|
428
|
+
: session.execution.reasoningEffort;
|
|
429
|
+
if (nextModel !== session.execution.model ||
|
|
430
|
+
nextReasoningEffort !== session.execution.reasoningEffort) {
|
|
431
|
+
await this.services.sessionExecution.update({
|
|
432
|
+
sessionId,
|
|
433
|
+
model: nextModel,
|
|
434
|
+
reasoningEffort: nextReasoningEffort,
|
|
435
|
+
});
|
|
436
|
+
session = this.ownedSession(sessionId);
|
|
437
|
+
}
|
|
438
|
+
const content = hasContent ? await pluginRunContent(input.content) : undefined;
|
|
333
439
|
const abort = new AbortController();
|
|
334
440
|
const cancel = () => abort.abort(signal?.reason);
|
|
335
441
|
if (signal?.aborted) {
|
|
@@ -337,15 +443,35 @@ export class PluginHostRpc {
|
|
|
337
443
|
throwIfHostCallAborted(signal);
|
|
338
444
|
}
|
|
339
445
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
340
|
-
const
|
|
446
|
+
const runInput = new PluginRunInputController();
|
|
341
447
|
const runId = randomUUID();
|
|
448
|
+
let resolveStarted;
|
|
449
|
+
let rejectStarted;
|
|
450
|
+
const startedResult = new Promise((resolve, reject) => {
|
|
451
|
+
resolveStarted = resolve;
|
|
452
|
+
rejectStarted = reject;
|
|
453
|
+
});
|
|
454
|
+
// The initiating RPC observes startup directly below. This promise exists
|
|
455
|
+
// for concurrent/replayed calls, so retain its rejection without creating
|
|
456
|
+
// an unhandled-rejection when there are none.
|
|
457
|
+
void startedResult.catch(() => undefined);
|
|
458
|
+
let resolveSettled;
|
|
459
|
+
const settled = new Promise((resolve) => {
|
|
460
|
+
resolveSettled = resolve;
|
|
461
|
+
});
|
|
342
462
|
const run = {
|
|
343
463
|
runId,
|
|
344
464
|
sessionId,
|
|
345
|
-
operationId,
|
|
465
|
+
operationIds: new Set(operationId ? [operationId] : []),
|
|
346
466
|
interruptRequested: false,
|
|
347
467
|
abort,
|
|
348
|
-
|
|
468
|
+
input: runInput,
|
|
469
|
+
started: startedResult,
|
|
470
|
+
startupSettled: false,
|
|
471
|
+
resolveStarted,
|
|
472
|
+
rejectStarted,
|
|
473
|
+
settled,
|
|
474
|
+
resolveSettled,
|
|
349
475
|
};
|
|
350
476
|
this.activeRuns.set(runId, run);
|
|
351
477
|
this.activeSessionRuns.set(sessionId, run);
|
|
@@ -353,16 +479,23 @@ export class PluginHostRpc {
|
|
|
353
479
|
this.activeRunsByOperation.set(operationId, run);
|
|
354
480
|
let started;
|
|
355
481
|
try {
|
|
356
|
-
started = await this.services.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
482
|
+
started = await this.services.sessionRuns.startRun(sessionId, message !== undefined
|
|
483
|
+
? {
|
|
484
|
+
message,
|
|
485
|
+
clientMessageId: operationId ?? `plugin_run_${runId}`,
|
|
486
|
+
}
|
|
487
|
+
: {
|
|
488
|
+
content: content,
|
|
489
|
+
clientMessageId: operationId ?? `plugin_run_${runId}`,
|
|
490
|
+
}, {
|
|
361
491
|
signal: abort.signal,
|
|
362
|
-
steer,
|
|
492
|
+
steer: runInput,
|
|
493
|
+
...(collaborationMode ? { collaborationMode } : {}),
|
|
363
494
|
});
|
|
364
495
|
}
|
|
365
496
|
catch (error) {
|
|
497
|
+
run.startupSettled = true;
|
|
498
|
+
run.rejectStarted(error);
|
|
366
499
|
this.releaseRun(run);
|
|
367
500
|
throw error;
|
|
368
501
|
}
|
|
@@ -371,20 +504,30 @@ export class PluginHostRpc {
|
|
|
371
504
|
}
|
|
372
505
|
if (signal?.aborted || this.disposed || this.activeRuns.get(runId) !== run) {
|
|
373
506
|
abort.abort(signal?.reason);
|
|
507
|
+
const error = signal?.aborted
|
|
508
|
+
? new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled")
|
|
509
|
+
: new PluginHostCallError("HOST_CLOSED", "plugin host is closed");
|
|
510
|
+
run.startupSettled = true;
|
|
511
|
+
run.rejectStarted(error);
|
|
374
512
|
this.releaseRun(run);
|
|
375
513
|
throwIfHostCallAborted(signal);
|
|
376
|
-
throw
|
|
514
|
+
throw error;
|
|
377
515
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
381
|
-
return {
|
|
516
|
+
run.responseId = started.responseId;
|
|
517
|
+
const result = {
|
|
382
518
|
runId,
|
|
519
|
+
responseId: started.responseId,
|
|
383
520
|
sessionId,
|
|
384
521
|
provider: started.provider,
|
|
385
522
|
model: started.model,
|
|
386
523
|
authSource: started.authSource,
|
|
387
524
|
};
|
|
525
|
+
run.startupSettled = true;
|
|
526
|
+
run.resolveStarted(result);
|
|
527
|
+
// Let the RPC transport write the request response before stream events are
|
|
528
|
+
// emitted. `runLive` has already subscribed, so no agent event is lost.
|
|
529
|
+
setImmediate(() => void this.pumpRun(run, started.generator));
|
|
530
|
+
return result;
|
|
388
531
|
}
|
|
389
532
|
async pumpRun(run, generator) {
|
|
390
533
|
let terminalFailure;
|
|
@@ -403,6 +546,16 @@ export class PluginHostRpc {
|
|
|
403
546
|
for (const projected of projectSessionEvent(run, event)) {
|
|
404
547
|
await this.emit(projected);
|
|
405
548
|
}
|
|
549
|
+
try {
|
|
550
|
+
const fileEvent = await this.projectSessionFileEvent(run, event);
|
|
551
|
+
if (fileEvent)
|
|
552
|
+
await this.emit(fileEvent);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// File delivery is an optional projection of a canonical event. A
|
|
556
|
+
// missing borrowed path must not turn an otherwise successful native
|
|
557
|
+
// Turn into session.run.failed or stop consuming its event stream.
|
|
558
|
+
}
|
|
406
559
|
terminalFailure = sessionRunFailure(event);
|
|
407
560
|
if (terminalFailure)
|
|
408
561
|
break;
|
|
@@ -465,28 +618,83 @@ export class PluginHostRpc {
|
|
|
465
618
|
this.releaseRun(run);
|
|
466
619
|
}
|
|
467
620
|
}
|
|
621
|
+
async projectSessionFileEvent(run, event) {
|
|
622
|
+
const input = jsonRecord(event);
|
|
623
|
+
if (input?.type !== "response.output_item.done")
|
|
624
|
+
return undefined;
|
|
625
|
+
const item = jsonRecord(input.item);
|
|
626
|
+
if (item?.type !== "function_call_output")
|
|
627
|
+
return undefined;
|
|
628
|
+
const data = jsonRecord(item.data);
|
|
629
|
+
const image = jsonRecord(data?.generatedImage);
|
|
630
|
+
if (!image || typeof image.mediaType !== "string")
|
|
631
|
+
return undefined;
|
|
632
|
+
let filePath;
|
|
633
|
+
if (typeof image.path === "string" && path.isAbsolute(image.path)) {
|
|
634
|
+
filePath = image.path;
|
|
635
|
+
}
|
|
636
|
+
else if (typeof image.resourceId === "string") {
|
|
637
|
+
filePath = await this.services.sessionFiles.resolveResourcePath(run.sessionId, image.resourceId);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
event: "session.run.file",
|
|
644
|
+
payload: {
|
|
645
|
+
runId: run.runId,
|
|
646
|
+
responseId: typeof item.responseId === "string"
|
|
647
|
+
? item.responseId
|
|
648
|
+
: run.responseId,
|
|
649
|
+
sessionId: run.sessionId,
|
|
650
|
+
itemId: typeof item.id === "string" ? item.id : "unknown",
|
|
651
|
+
filename: typeof image.filename === "string" && image.filename
|
|
652
|
+
? image.filename
|
|
653
|
+
: path.basename(filePath),
|
|
654
|
+
mediaType: image.mediaType,
|
|
655
|
+
path: filePath,
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
}
|
|
468
659
|
releaseRun(run) {
|
|
660
|
+
run.input.close();
|
|
661
|
+
if (!run.startupSettled) {
|
|
662
|
+
run.startupSettled = true;
|
|
663
|
+
run.rejectStarted(new PluginHostCallError("HOST_CLOSED", "plugin Host closed the active Run"));
|
|
664
|
+
}
|
|
469
665
|
if (this.activeRuns.get(run.runId) === run)
|
|
470
666
|
this.activeRuns.delete(run.runId);
|
|
471
667
|
if (this.activeSessionRuns.get(run.sessionId) === run) {
|
|
472
668
|
this.activeSessionRuns.delete(run.sessionId);
|
|
473
669
|
}
|
|
474
|
-
|
|
475
|
-
this.activeRunsByOperation.
|
|
670
|
+
for (const operationId of run.operationIds) {
|
|
671
|
+
if (this.activeRunsByOperation.get(operationId) === run) {
|
|
672
|
+
this.activeRunsByOperation.delete(operationId);
|
|
673
|
+
}
|
|
476
674
|
}
|
|
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) };
|
|
675
|
+
run.resolveSettled();
|
|
483
676
|
}
|
|
484
677
|
async interruptRun(params) {
|
|
485
678
|
const input = record(params);
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
679
|
+
const sessionId = optionalString(input.sessionId, "sessionId", 128);
|
|
680
|
+
const runId = optionalString(input.runId, "runId", 128);
|
|
681
|
+
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
682
|
+
if ((sessionId ? 1 : 0) + (runId ? 1 : 0) + (operationId ? 1 : 0) !== 1) {
|
|
683
|
+
throw new PluginHostCallError("INVALID_PARAMS", "exactly one of sessionId, runId, or operationId is required");
|
|
684
|
+
}
|
|
685
|
+
let run;
|
|
686
|
+
if (sessionId) {
|
|
687
|
+
this.ownedSession(sessionId);
|
|
688
|
+
run = this.activeSessionRuns.get(sessionId);
|
|
689
|
+
if (!run) {
|
|
690
|
+
const interrupted = this.services.interruptSession
|
|
691
|
+
? Promise.resolve().then(() => this.services.interruptSession(sessionId))
|
|
692
|
+
: Promise.resolve(false);
|
|
693
|
+
return { interrupted: await interrupted };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
run = this.activeRunByIdentity(input);
|
|
490
698
|
}
|
|
491
699
|
if (run.interruptResult) {
|
|
492
700
|
return { interrupted: await run.interruptResult };
|
|
@@ -506,9 +714,7 @@ export class PluginHostRpc {
|
|
|
506
714
|
// effort and cannot prove that native execution stopped.
|
|
507
715
|
return false;
|
|
508
716
|
});
|
|
509
|
-
const interruptResult =
|
|
510
|
-
? graceful
|
|
511
|
-
: forceStoppedInterrupt(graceful, forceAfterMs, () => this.services.terminateSession?.(run.sessionId) ?? Promise.resolve(false));
|
|
717
|
+
const interruptResult = graceful;
|
|
512
718
|
run.interruptResult = interruptResult;
|
|
513
719
|
try {
|
|
514
720
|
const interrupted = await interruptResult;
|
|
@@ -526,6 +732,17 @@ export class PluginHostRpc {
|
|
|
526
732
|
throw error;
|
|
527
733
|
}
|
|
528
734
|
}
|
|
735
|
+
async sessionActivity(params) {
|
|
736
|
+
const sessionId = requiredSessionId(params);
|
|
737
|
+
this.ownedSession(sessionId);
|
|
738
|
+
const snapshot = structuredClone(await this.services.sessionActivity(sessionId));
|
|
739
|
+
const waiting = snapshot.pendingInteractions.length > 0;
|
|
740
|
+
return {
|
|
741
|
+
status: waiting ? "waiting" : snapshot.status,
|
|
742
|
+
activeResponseIds: snapshot.activeResponseIds,
|
|
743
|
+
pendingInteractions: snapshot.pendingInteractions,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
529
746
|
async interruptWon(run) {
|
|
530
747
|
if (!run.interruptRequested)
|
|
531
748
|
return false;
|
|
@@ -533,13 +750,6 @@ export class PluginHostRpc {
|
|
|
533
750
|
return true;
|
|
534
751
|
return run.interruptResult.catch(() => false);
|
|
535
752
|
}
|
|
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
753
|
activeRunByIdentity(input) {
|
|
544
754
|
const runId = optionalString(input.runId, "runId", 128);
|
|
545
755
|
const operationId = optionalString(input.operationId, "operationId", 512);
|
|
@@ -652,6 +862,17 @@ export class PluginHostRpc {
|
|
|
652
862
|
...(ttlSeconds === undefined ? {} : { ttlMs: ttlSeconds * 1_000 }),
|
|
653
863
|
});
|
|
654
864
|
}
|
|
865
|
+
issueSessionPortalLink(scope, access, sessionId) {
|
|
866
|
+
if (!this.services.sessionPortal) {
|
|
867
|
+
throw new PluginHostCallError("HOST_CALL_FAILED", "the Session Portal is not configured");
|
|
868
|
+
}
|
|
869
|
+
return issueSessionPortalTicket({
|
|
870
|
+
pluginId: this.principalId,
|
|
871
|
+
scope,
|
|
872
|
+
access,
|
|
873
|
+
...(sessionId ? { sessionId } : {}),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
655
876
|
ownedSession(sessionId) {
|
|
656
877
|
const session = this.services.sessionRegistry.get(sessionId);
|
|
657
878
|
if (!session || !this.isOwned(session)) {
|
|
@@ -678,6 +899,24 @@ function throwIfHostCallAborted(signal) {
|
|
|
678
899
|
return;
|
|
679
900
|
throw new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled");
|
|
680
901
|
}
|
|
902
|
+
async function waitForHostPromise(promise, signal) {
|
|
903
|
+
throwIfHostCallAborted(signal);
|
|
904
|
+
if (!signal)
|
|
905
|
+
return await promise;
|
|
906
|
+
return await new Promise((resolve, reject) => {
|
|
907
|
+
const aborted = () => {
|
|
908
|
+
reject(new PluginHostCallError("REQUEST_CANCELLED", "plugin host request was cancelled"));
|
|
909
|
+
};
|
|
910
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
911
|
+
void promise.then((value) => {
|
|
912
|
+
signal.removeEventListener("abort", aborted);
|
|
913
|
+
resolve(value);
|
|
914
|
+
}, (error) => {
|
|
915
|
+
signal.removeEventListener("abort", aborted);
|
|
916
|
+
reject(error);
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
}
|
|
681
920
|
export class PluginHostCallError extends PluginRunnerError {
|
|
682
921
|
retryable;
|
|
683
922
|
data;
|
|
@@ -700,6 +939,12 @@ function toHostCallError(error) {
|
|
|
700
939
|
// not be brought up, so callers may fail without quarantining capacity.
|
|
701
940
|
return new PluginHostCallError("LIVE_SESSION_UNAVAILABLE", error.message, false);
|
|
702
941
|
}
|
|
942
|
+
if (error instanceof AgentRuntimeError) {
|
|
943
|
+
// Native lifecycle failures already carry a stable phase code. Preserve it
|
|
944
|
+
// across the plugin-host boundary instead of erasing it behind the generic
|
|
945
|
+
// HOST_CALL_FAILED wrapper.
|
|
946
|
+
return new PluginHostCallError(error.code, error.message, false);
|
|
947
|
+
}
|
|
703
948
|
if (error instanceof SessionLaunchOperationConflictError) {
|
|
704
949
|
return new PluginHostCallError(error.code, error.message, false);
|
|
705
950
|
}
|
|
@@ -717,7 +962,10 @@ function sessionLaunchSelection(value) {
|
|
|
717
962
|
const agent = optionalAgentId(input.agent, "session.agent");
|
|
718
963
|
const config = input.config === undefined ? undefined : agentSpecSchema.parse(input.config);
|
|
719
964
|
const provider = optionalRuntimeValue(input.provider);
|
|
720
|
-
const
|
|
965
|
+
const modelProvided = input.model !== undefined;
|
|
966
|
+
const model = input.model === null || input.model === ""
|
|
967
|
+
? null
|
|
968
|
+
: optionalString(input.model, "session.model", 256);
|
|
721
969
|
const reasoningEffort = optionalString(input.reasoningEffort, "session.reasoningEffort", 128);
|
|
722
970
|
const title = optionalString(input.title, "session.title", 1_000);
|
|
723
971
|
if (Number(Boolean(projectId)) + Number(Boolean(managedCwd)) + Number(Boolean(workspace)) > 1) {
|
|
@@ -736,7 +984,7 @@ function sessionLaunchSelection(value) {
|
|
|
736
984
|
...(agent ? { agent } : {}),
|
|
737
985
|
...(config ? { config } : {}),
|
|
738
986
|
...(provider ? { provider } : {}),
|
|
739
|
-
...(
|
|
987
|
+
...(modelProvided ? { model: model ?? null } : {}),
|
|
740
988
|
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
741
989
|
...(title ? { title } : {}),
|
|
742
990
|
};
|
|
@@ -768,6 +1016,99 @@ function sessionExecutionSnapshot(value) {
|
|
|
768
1016
|
function requiredSessionId(params) {
|
|
769
1017
|
return requiredString(record(params).sessionId, "sessionId", 512);
|
|
770
1018
|
}
|
|
1019
|
+
async function pluginRunContent(value) {
|
|
1020
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
|
|
1021
|
+
throw new PluginHostCallError("INVALID_PARAMS", "content must be a non-empty array with at most 256 parts");
|
|
1022
|
+
}
|
|
1023
|
+
return Promise.all(value.map(async (entry, index) => {
|
|
1024
|
+
const input = record(entry);
|
|
1025
|
+
if (input.type === "input_text") {
|
|
1026
|
+
return {
|
|
1027
|
+
type: "input_text",
|
|
1028
|
+
text: requiredText(input.text, `content[${index}].text`, MAX_MESSAGE_LENGTH),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (input.type !== "input_image" && input.type !== "input_file") {
|
|
1032
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].type is invalid`);
|
|
1033
|
+
}
|
|
1034
|
+
const suppliedPath = requiredPathString(input.path, `content[${index}].path`, 16_384);
|
|
1035
|
+
if (!path.isAbsolute(suppliedPath)) {
|
|
1036
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path must be absolute`);
|
|
1037
|
+
}
|
|
1038
|
+
let resolvedPath;
|
|
1039
|
+
let fileSize;
|
|
1040
|
+
try {
|
|
1041
|
+
resolvedPath = await realpath(suppliedPath);
|
|
1042
|
+
const metadata = await stat(resolvedPath);
|
|
1043
|
+
if (!metadata.isFile())
|
|
1044
|
+
throw new Error("not a regular file");
|
|
1045
|
+
fileSize = metadata.size;
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
throw new PluginHostCallError("INVALID_PARAMS", `content[${index}].path is not a readable regular file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1049
|
+
}
|
|
1050
|
+
const filename = input.type === "input_file"
|
|
1051
|
+
? requiredString(input.filename, `content[${index}].filename`, 255)
|
|
1052
|
+
: optionalString(input.filename, `content[${index}].filename`, 255) ??
|
|
1053
|
+
path.basename(resolvedPath);
|
|
1054
|
+
if (input.type === "input_image") {
|
|
1055
|
+
return {
|
|
1056
|
+
type: "input_image",
|
|
1057
|
+
path: resolvedPath,
|
|
1058
|
+
filename,
|
|
1059
|
+
mediaType: pluginImageMediaType(input.mediaType, resolvedPath, `content[${index}].mediaType`),
|
|
1060
|
+
byteLength: fileSize,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
type: "input_file",
|
|
1065
|
+
path: resolvedPath,
|
|
1066
|
+
filename,
|
|
1067
|
+
mediaType: pluginMediaType(input.mediaType ?? "application/octet-stream", `content[${index}].mediaType`),
|
|
1068
|
+
byteLength: fileSize,
|
|
1069
|
+
};
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
function pluginImageMediaType(value, filePath, label) {
|
|
1073
|
+
const inferred = (() => {
|
|
1074
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
1075
|
+
case ".png": return "image/png";
|
|
1076
|
+
case ".jpg":
|
|
1077
|
+
case ".jpeg": return "image/jpeg";
|
|
1078
|
+
case ".webp": return "image/webp";
|
|
1079
|
+
case ".gif": return "image/gif";
|
|
1080
|
+
case ".svg": return "image/svg+xml";
|
|
1081
|
+
default: return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
})();
|
|
1084
|
+
const mediaType = value === undefined ? inferred : pluginMediaType(value, label);
|
|
1085
|
+
if (mediaType !== "image/png" &&
|
|
1086
|
+
mediaType !== "image/jpeg" &&
|
|
1087
|
+
mediaType !== "image/webp" &&
|
|
1088
|
+
mediaType !== "image/gif" &&
|
|
1089
|
+
mediaType !== "image/svg+xml") {
|
|
1090
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is not a supported image type`);
|
|
1091
|
+
}
|
|
1092
|
+
return mediaType;
|
|
1093
|
+
}
|
|
1094
|
+
function pluginMediaType(value, label) {
|
|
1095
|
+
if (typeof value !== "string") {
|
|
1096
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} must be a media type`);
|
|
1097
|
+
}
|
|
1098
|
+
const mediaType = value.trim().toLowerCase();
|
|
1099
|
+
if (mediaType.length === 0 ||
|
|
1100
|
+
mediaType.length > 255 ||
|
|
1101
|
+
!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType)) {
|
|
1102
|
+
throw new PluginHostCallError("INVALID_PARAMS", `${label} is invalid`);
|
|
1103
|
+
}
|
|
1104
|
+
return mediaType;
|
|
1105
|
+
}
|
|
1106
|
+
function requiredSessionPortalAccess(params) {
|
|
1107
|
+
const access = record(params).access;
|
|
1108
|
+
if (access === "read_only" || access === "read_write")
|
|
1109
|
+
return access;
|
|
1110
|
+
throw new PluginHostCallError("INVALID_PARAMS", "access must be read_only or read_write");
|
|
1111
|
+
}
|
|
771
1112
|
function sessionInteractionResolution(value) {
|
|
772
1113
|
const input = record(value);
|
|
773
1114
|
const keys = Object.keys(input);
|
|
@@ -810,6 +1151,13 @@ function sessionInteractionResolution(value) {
|
|
|
810
1151
|
function optionalRuntime(params) {
|
|
811
1152
|
return optionalRuntimeValue(record(params).runtime);
|
|
812
1153
|
}
|
|
1154
|
+
function optionalCollaborationMode(value) {
|
|
1155
|
+
if (value === undefined)
|
|
1156
|
+
return undefined;
|
|
1157
|
+
if (value === "plan" || value === "default")
|
|
1158
|
+
return value;
|
|
1159
|
+
throw new PluginHostCallError("INVALID_PARAMS", "collaborationMode must be plan or default");
|
|
1160
|
+
}
|
|
813
1161
|
function optionalRuntimeValue(value) {
|
|
814
1162
|
if (value === undefined || value === null || value === "")
|
|
815
1163
|
return undefined;
|