@omercnet/paseo-omp 5.1.1 → 5.2.0-next.362.1
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/OVERVIEW.md +1 -0
- package/README.md +2 -2
- package/client/provider-image.tsx +127 -14
- package/client/provider-launch-settings.tsx +46 -2
- package/client/subagents-state.ts +108 -0
- package/client/subagents-strip.tsx +391 -0
- package/index.client.tsx +9 -0
- package/index.server.ts +25 -0
- package/npm-shrinkwrap.json +9 -9
- package/package.json +6 -9
- package/scripts/prepare-dependencies.mjs +7 -10
- package/server/mcp-browser.ts +3 -1
- package/server/package-version.ts +1 -1
- package/server/provider/connection.ts +43 -8
- package/server/provider/omp-rpc-process.ts +9 -9
- package/server/provider/omp-rpc-protocol.ts +40 -2
- package/server/provider/omp-rpc-transport.ts +5 -2
- package/server/provider/omp-rpc.ts +25 -0
- package/server/provider/registration.ts +18 -0
- package/server/provider/session-compaction.ts +29 -25
- package/server/provider/session-descriptors.ts +36 -30
- package/server/provider/session-events.ts +1 -0
- package/server/provider/session-history.ts +2 -0
- package/server/provider/session-limit.ts +32 -0
- package/server/provider/session-terminal.ts +52 -1
- package/server/provider/session.ts +255 -67
- package/server/provider/subsessions.ts +109 -7
- package/server/provider/timeline-projector.ts +41 -26
- package/server/subagents.ts +97 -0
- package/shared/provider-launch-settings.ts +4 -0
- package/shared/subagents.ts +60 -0
|
@@ -2,6 +2,12 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { basename, extname, isAbsolute } from "node:path";
|
|
3
3
|
import type { ProviderEvent } from "@getpaseo/plugin/server/provider";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
MAX_OMP_SUBAGENT_STRIP_ENTRIES,
|
|
7
|
+
OMP_SUBAGENT_DESCRIPTION_MAX_LENGTH,
|
|
8
|
+
OMP_SUBAGENT_STRIP_KIND,
|
|
9
|
+
type OmpSubagentStrip,
|
|
10
|
+
} from "../../shared/subagents";
|
|
5
11
|
import type { OmpRuntime, OmpRuntimeSession } from "./omp-rpc";
|
|
6
12
|
import type {
|
|
7
13
|
OmpAgentSessionEvent,
|
|
@@ -56,6 +62,7 @@ type ChildState = {
|
|
|
56
62
|
sessionClosed: boolean;
|
|
57
63
|
replayUnavailable?: boolean;
|
|
58
64
|
seenInSnapshot: boolean;
|
|
65
|
+
runningSince: number;
|
|
59
66
|
seenAssistantIdentities: BoundedStringSet;
|
|
60
67
|
projector: OmpTimelineProjector;
|
|
61
68
|
};
|
|
@@ -305,6 +312,19 @@ function terminalStatus(status: string): ChildTerminalStatus | undefined {
|
|
|
305
312
|
return;
|
|
306
313
|
}
|
|
307
314
|
|
|
315
|
+
/**
|
|
316
|
+
* OMP derives subagent IDs from model-chosen task names, so clients only see this opaque handle,
|
|
317
|
+
* scoped to the provider session that owns the subagent.
|
|
318
|
+
*/
|
|
319
|
+
export function ompSubagentHandle(sessionId: string, nativeId: string): string {
|
|
320
|
+
return createHash("sha256")
|
|
321
|
+
.update(sessionId)
|
|
322
|
+
.update("\0")
|
|
323
|
+
.update(nativeId)
|
|
324
|
+
.digest("base64url")
|
|
325
|
+
.slice(0, 32);
|
|
326
|
+
}
|
|
327
|
+
|
|
308
328
|
function bufferedNativeId(event: OmpSubagentEvent): string {
|
|
309
329
|
return event.type === "subagent_progress" ? event.payload.progress.id : event.payload.id;
|
|
310
330
|
}
|
|
@@ -314,6 +334,10 @@ export class OmpSubsessionProjector {
|
|
|
314
334
|
private readonly sessionIdByNativeId = new Map<string, string>();
|
|
315
335
|
private readonly toolOwners = new Map<string, string>();
|
|
316
336
|
private readonly dispatches = new Map<string, TaskDispatch>();
|
|
337
|
+
/** Last published payload of each non-empty subagent control strip, by timeline item ID. */
|
|
338
|
+
private readonly controlStrips = new Map<string, string>();
|
|
339
|
+
/** Root `task` calls whose tool card the root timeline has published, oldest first. */
|
|
340
|
+
private readonly anchoredRootTasks = new Set<string>();
|
|
317
341
|
private readonly bufferedEvents: BufferedSubagentEvent[] = [];
|
|
318
342
|
// null means every child observed during this replay is omitted after tombstone saturation.
|
|
319
343
|
private omittedBufferedChildren: Set<string> | null = new Set();
|
|
@@ -368,7 +392,7 @@ export class OmpSubsessionProjector {
|
|
|
368
392
|
if (event.isError) {
|
|
369
393
|
this.dispatches.delete(event.toolCallId);
|
|
370
394
|
this.toolOwners.delete(event.toolCallId);
|
|
371
|
-
this.
|
|
395
|
+
this.activityChanged();
|
|
372
396
|
return;
|
|
373
397
|
}
|
|
374
398
|
dispatch.acknowledged = true;
|
|
@@ -462,6 +486,82 @@ export class OmpSubsessionProjector {
|
|
|
462
486
|
this.reconcileSnapshots(await runtime.getSubagents());
|
|
463
487
|
}
|
|
464
488
|
|
|
489
|
+
private activityChanged(): void {
|
|
490
|
+
this.publishControlStrips();
|
|
491
|
+
this.onActivityChange();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Marks a root `task` call whose tool card is now in the root timeline. Its control strip is
|
|
496
|
+
* published only after this, so the strip lands below the card even when subagent frames
|
|
497
|
+
* arrive while the turn's own events are still buffered.
|
|
498
|
+
*/
|
|
499
|
+
anchorRootTask(toolCallId: string): void {
|
|
500
|
+
if (this.anchoredRootTasks.size >= MAX_TASK_DISPATCHES) {
|
|
501
|
+
const oldest = this.anchoredRootTasks.values().next().value;
|
|
502
|
+
if (oldest !== undefined) this.anchoredRootTasks.delete(oldest);
|
|
503
|
+
}
|
|
504
|
+
this.anchoredRootTasks.add(toolCallId);
|
|
505
|
+
this.publishControlStrips();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Publishes one plugin timeline row per root `task` call, below its tool card, listing the
|
|
510
|
+
* call's running subagents so the client can steer or cancel them in place. A row is emptied
|
|
511
|
+
* rather than removed when its subagents finish.
|
|
512
|
+
*/
|
|
513
|
+
private publishControlStrips(): void {
|
|
514
|
+
if (!this.pluginTimelineEnabled || this.closed) return;
|
|
515
|
+
const strips = new Map<string, OmpSubagentStrip["subagents"]>();
|
|
516
|
+
for (const child of this.children.values()) {
|
|
517
|
+
if (child.status !== "running") continue;
|
|
518
|
+
const taskCallId = this.rootTaskCallId(child);
|
|
519
|
+
if (!taskCallId || !this.anchoredRootTasks.has(taskCallId)) continue;
|
|
520
|
+
const id = `omp:subagents:${taskCallId}`;
|
|
521
|
+
const entries = strips.get(id) ?? [];
|
|
522
|
+
if (entries.length >= MAX_OMP_SUBAGENT_STRIP_ENTRIES) continue;
|
|
523
|
+
entries.push({
|
|
524
|
+
id: ompSubagentHandle(this.rootSessionId, child.nativeId),
|
|
525
|
+
agent: child.title,
|
|
526
|
+
description: child.description
|
|
527
|
+
? this.dataFilter.text(child.description, OMP_SUBAGENT_DESCRIPTION_MAX_LENGTH)
|
|
528
|
+
: null,
|
|
529
|
+
runningSince: child.runningSince,
|
|
530
|
+
});
|
|
531
|
+
strips.set(id, entries);
|
|
532
|
+
}
|
|
533
|
+
for (const id of new Set([...this.controlStrips.keys(), ...strips.keys()])) {
|
|
534
|
+
const data: OmpSubagentStrip = { subagents: strips.get(id) ?? [] };
|
|
535
|
+
const payload = JSON.stringify(data);
|
|
536
|
+
if (this.controlStrips.get(id) === payload) continue;
|
|
537
|
+
if (data.subagents.length > 0) this.controlStrips.set(id, payload);
|
|
538
|
+
else this.controlStrips.delete(id);
|
|
539
|
+
this.emit({
|
|
540
|
+
type: "timeline.item",
|
|
541
|
+
sessionId: this.rootSessionId,
|
|
542
|
+
item: {
|
|
543
|
+
type: "plugin",
|
|
544
|
+
id,
|
|
545
|
+
pluginId: "paseo-omp",
|
|
546
|
+
kind: OMP_SUBAGENT_STRIP_KIND,
|
|
547
|
+
version: 1,
|
|
548
|
+
data,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** The root timeline's `task` call that a subagent descends from, when it is known. */
|
|
555
|
+
private rootTaskCallId(child: ChildState): string | undefined {
|
|
556
|
+
let current = child;
|
|
557
|
+
for (let depth = 0; current.parentSessionId !== this.rootSessionId; depth += 1) {
|
|
558
|
+
const parent = this.children.get(current.parentSessionId);
|
|
559
|
+
if (!parent || depth >= MAX_CHILDREN) return;
|
|
560
|
+
current = parent;
|
|
561
|
+
}
|
|
562
|
+
return current.parentToolCallId;
|
|
563
|
+
}
|
|
564
|
+
|
|
465
565
|
hasActiveChildren(): boolean {
|
|
466
566
|
for (const child of this.children.values()) if (child.status === "running") return true;
|
|
467
567
|
for (const dispatch of this.dispatches.values()) if (dispatch.acknowledged) return true;
|
|
@@ -474,7 +574,7 @@ export class OmpSubsessionProjector {
|
|
|
474
574
|
}
|
|
475
575
|
this.dispatches.clear();
|
|
476
576
|
this.toolOwners.clear();
|
|
477
|
-
this.
|
|
577
|
+
this.activityChanged();
|
|
478
578
|
}
|
|
479
579
|
|
|
480
580
|
close(): void {
|
|
@@ -626,7 +726,7 @@ export class OmpSubsessionProjector {
|
|
|
626
726
|
const terminal = terminalStatus(event.payload.status);
|
|
627
727
|
if (terminal) this.requestTerminal(child, terminal);
|
|
628
728
|
else if (event.payload.status === "started") this.restartChild(child);
|
|
629
|
-
this.
|
|
729
|
+
this.activityChanged();
|
|
630
730
|
return;
|
|
631
731
|
}
|
|
632
732
|
if (event.type === "subagent_progress") {
|
|
@@ -643,7 +743,7 @@ export class OmpSubsessionProjector {
|
|
|
643
743
|
const terminal = terminalStatus(event.payload.progress.status);
|
|
644
744
|
if (terminal) this.requestTerminal(child, terminal);
|
|
645
745
|
else this.restartChild(child);
|
|
646
|
-
this.
|
|
746
|
+
this.activityChanged();
|
|
647
747
|
return;
|
|
648
748
|
}
|
|
649
749
|
const sessionId = this.sessionIdByNativeId.get(event.payload.id);
|
|
@@ -701,6 +801,7 @@ export class OmpSubsessionProjector {
|
|
|
701
801
|
...(ref.sessionFile ? { sessionFile: ref.sessionFile } : {}),
|
|
702
802
|
...(ref.parentToolCallId ? { parentToolCallId: ref.parentToolCallId } : {}),
|
|
703
803
|
status: "running",
|
|
804
|
+
runningSince: Date.now(),
|
|
704
805
|
sessionClosed: false,
|
|
705
806
|
seenAssistantIdentities: new BoundedStringSet(MAX_CHILD_MESSAGE_IDENTITIES),
|
|
706
807
|
seenInSnapshot: false,
|
|
@@ -739,6 +840,7 @@ export class OmpSubsessionProjector {
|
|
|
739
840
|
child.replayUnavailable = false;
|
|
740
841
|
if (child.status === "running") return;
|
|
741
842
|
child.status = "running";
|
|
843
|
+
child.runningSince = Date.now();
|
|
742
844
|
child.terminalRequested = undefined;
|
|
743
845
|
child.seenInSnapshot = false;
|
|
744
846
|
child.turnSequence += 1;
|
|
@@ -784,7 +886,7 @@ export class OmpSubsessionProjector {
|
|
|
784
886
|
if (parent?.terminalRequested && !this.hasDirectActivity(parent.sessionId)) {
|
|
785
887
|
this.finishChild(parent, parent.terminalRequested);
|
|
786
888
|
}
|
|
787
|
-
this.
|
|
889
|
+
this.activityChanged();
|
|
788
890
|
}
|
|
789
891
|
private failReplayChild(child: ChildState): void {
|
|
790
892
|
child.replayUnavailable = true;
|
|
@@ -812,7 +914,7 @@ export class OmpSubsessionProjector {
|
|
|
812
914
|
if (owner?.terminalRequested && !this.hasDirectActivity(owner.sessionId)) {
|
|
813
915
|
this.finishChild(owner, owner.terminalRequested);
|
|
814
916
|
}
|
|
815
|
-
this.
|
|
917
|
+
this.activityChanged();
|
|
816
918
|
}
|
|
817
919
|
|
|
818
920
|
private resolveParent(parentToolCallId?: string, sessionFile?: string): string {
|
|
@@ -864,7 +966,7 @@ export class OmpSubsessionProjector {
|
|
|
864
966
|
this.requestTerminal(child, "completed");
|
|
865
967
|
}
|
|
866
968
|
}
|
|
867
|
-
this.
|
|
969
|
+
this.activityChanged();
|
|
868
970
|
}
|
|
869
971
|
|
|
870
972
|
private async replaySnapshots(
|
|
@@ -372,8 +372,8 @@ export class OmpTimelineProjector {
|
|
|
372
372
|
private flushTimer: unknown;
|
|
373
373
|
private currentTurnId: string | null = null;
|
|
374
374
|
private assistantSequence = 0;
|
|
375
|
+
// Least recently used first, so a long turn evicts its oldest identities.
|
|
375
376
|
private readonly turnNativeMessageIds = new Map<string, string>();
|
|
376
|
-
private nativeIdentitySaturated = false;
|
|
377
377
|
private assistantIdentitySequence = 0;
|
|
378
378
|
private noticeSequence = 0;
|
|
379
379
|
private toolSequence = 0;
|
|
@@ -467,6 +467,9 @@ export class OmpTimelineProjector {
|
|
|
467
467
|
event.type === "auto_compaction_end" ||
|
|
468
468
|
event.type === "compaction_start" ||
|
|
469
469
|
event.type === "compaction_end" ||
|
|
470
|
+
event.type === "manual_compaction_start" ||
|
|
471
|
+
event.type === "manual_compaction_end" ||
|
|
472
|
+
event.type === "extension_error" ||
|
|
470
473
|
event.type === "advisor_yielded" ||
|
|
471
474
|
event.type === "config_warnings_changed" ||
|
|
472
475
|
event.type === "advisor_cost_changed" ||
|
|
@@ -743,6 +746,19 @@ export class OmpTimelineProjector {
|
|
|
743
746
|
});
|
|
744
747
|
return;
|
|
745
748
|
}
|
|
749
|
+
if (event.type === "extension_error") {
|
|
750
|
+
this.noticeSequence += 1;
|
|
751
|
+
this.publish({
|
|
752
|
+
type: "notification",
|
|
753
|
+
id: `omp:extension-error:${this.noticeSequence}`,
|
|
754
|
+
level: "error",
|
|
755
|
+
message: this.dataFilter.text(
|
|
756
|
+
`OMP extension ${event.extensionPath} failed in ${event.event}: ${event.error}`,
|
|
757
|
+
64 * 1024,
|
|
758
|
+
),
|
|
759
|
+
});
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
746
762
|
if (event.type === "advisor_yielded") {
|
|
747
763
|
this.noticeSequence += 1;
|
|
748
764
|
this.publish({
|
|
@@ -753,7 +769,11 @@ export class OmpTimelineProjector {
|
|
|
753
769
|
});
|
|
754
770
|
return;
|
|
755
771
|
}
|
|
756
|
-
if (
|
|
772
|
+
if (
|
|
773
|
+
event.type === "auto_compaction_start" ||
|
|
774
|
+
event.type === "compaction_start" ||
|
|
775
|
+
event.type === "manual_compaction_start"
|
|
776
|
+
) {
|
|
757
777
|
const trigger = event.type === "auto_compaction_start" ? "auto" : "manual";
|
|
758
778
|
const action = event.type === "auto_compaction_start" ? event.action : undefined;
|
|
759
779
|
if (this.discardedCompactionEnds > 0) {
|
|
@@ -781,7 +801,11 @@ export class OmpTimelineProjector {
|
|
|
781
801
|
this.publish({ type: "compaction", id: slot.id, status: "loading", trigger });
|
|
782
802
|
return;
|
|
783
803
|
}
|
|
784
|
-
if (
|
|
804
|
+
if (
|
|
805
|
+
event.type === "auto_compaction_end" ||
|
|
806
|
+
event.type === "compaction_end" ||
|
|
807
|
+
event.type === "manual_compaction_end"
|
|
808
|
+
) {
|
|
785
809
|
if (this.discardedCompactionEnds > 0) {
|
|
786
810
|
this.discardedCompactionEnds -= 1;
|
|
787
811
|
return;
|
|
@@ -803,7 +827,7 @@ export class OmpTimelineProjector {
|
|
|
803
827
|
this.discardedCompactionEnds = 1;
|
|
804
828
|
return;
|
|
805
829
|
}
|
|
806
|
-
if (event.willRetry) {
|
|
830
|
+
if (event.type !== "manual_compaction_end" && event.willRetry) {
|
|
807
831
|
slot.retrying = true;
|
|
808
832
|
return;
|
|
809
833
|
}
|
|
@@ -826,10 +850,9 @@ export class OmpTimelineProjector {
|
|
|
826
850
|
type: "notification",
|
|
827
851
|
id: `${slot.id}:skipped`,
|
|
828
852
|
level: "warning",
|
|
829
|
-
message: "OMP compaction was skipped",
|
|
853
|
+
message: this.dataFilter.text(event.errorMessage ?? "OMP compaction was skipped", 4_096),
|
|
830
854
|
});
|
|
831
|
-
}
|
|
832
|
-
if (event.aborted || event.errorMessage) {
|
|
855
|
+
} else if (event.aborted || event.errorMessage) {
|
|
833
856
|
this.publish({
|
|
834
857
|
type: "error",
|
|
835
858
|
id: `${slot.id}:error`,
|
|
@@ -901,7 +924,6 @@ export class OmpTimelineProjector {
|
|
|
901
924
|
this.currentTurnId = null;
|
|
902
925
|
this.assistantSequence = 0;
|
|
903
926
|
this.turnNativeMessageIds.clear();
|
|
904
|
-
this.nativeIdentitySaturated = false;
|
|
905
927
|
this.assistantIdentitySequence = 0;
|
|
906
928
|
this.toolSequence = 0;
|
|
907
929
|
this.userSequence = 0;
|
|
@@ -1174,7 +1196,6 @@ export class OmpTimelineProjector {
|
|
|
1174
1196
|
this.currentTurnId = null;
|
|
1175
1197
|
this.assistantSequence = 0;
|
|
1176
1198
|
this.turnNativeMessageIds.clear();
|
|
1177
|
-
this.nativeIdentitySaturated = false;
|
|
1178
1199
|
}
|
|
1179
1200
|
|
|
1180
1201
|
close(): void {
|
|
@@ -1242,13 +1263,12 @@ export class OmpTimelineProjector {
|
|
|
1242
1263
|
this.commandPublishedText = "";
|
|
1243
1264
|
}
|
|
1244
1265
|
|
|
1245
|
-
private beginStream(message: OmpAssistantMessage, turnId: string): StreamSnapshot
|
|
1266
|
+
private beginStream(message: OmpAssistantMessage, turnId: string): StreamSnapshot {
|
|
1246
1267
|
this.assistantSequence += 1;
|
|
1247
1268
|
const nativeIdentity = assistantIdentity(message);
|
|
1248
1269
|
const messageId = nativeIdentity
|
|
1249
1270
|
? this.messageIdForNativeIdentity(nativeIdentity)
|
|
1250
1271
|
: this.nextAssistantMessageId(`turn:${turnId}:${this.assistantSequence}`);
|
|
1251
|
-
if (!messageId) return null;
|
|
1252
1272
|
this.stream = {
|
|
1253
1273
|
messageId,
|
|
1254
1274
|
...(nativeIdentity ? { nativeIdentity } : {}),
|
|
@@ -1261,15 +1281,16 @@ export class OmpTimelineProjector {
|
|
|
1261
1281
|
};
|
|
1262
1282
|
return this.stream;
|
|
1263
1283
|
}
|
|
1264
|
-
private messageIdForNativeIdentity(nativeIdentity: string): string
|
|
1284
|
+
private messageIdForNativeIdentity(nativeIdentity: string): string {
|
|
1265
1285
|
const existing = this.turnNativeMessageIds.get(nativeIdentity);
|
|
1266
|
-
if (existing)
|
|
1267
|
-
|
|
1268
|
-
this.
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1286
|
+
if (existing) {
|
|
1287
|
+
this.turnNativeMessageIds.delete(nativeIdentity);
|
|
1288
|
+
this.turnNativeMessageIds.set(nativeIdentity, existing);
|
|
1289
|
+
return existing;
|
|
1290
|
+
}
|
|
1291
|
+
if (this.turnNativeMessageIds.size >= MAX_TURN_NATIVE_IDENTITIES) {
|
|
1292
|
+
const oldest = this.turnNativeMessageIds.keys().next().value;
|
|
1293
|
+
if (oldest !== undefined) this.turnNativeMessageIds.delete(oldest);
|
|
1273
1294
|
}
|
|
1274
1295
|
const messageId = this.nextAssistantMessageId(nativeIdentity);
|
|
1275
1296
|
this.turnNativeMessageIds.set(nativeIdentity, messageId);
|
|
@@ -1290,12 +1311,7 @@ export class OmpTimelineProjector {
|
|
|
1290
1311
|
const nativeIdentity = assistantIdentity(message);
|
|
1291
1312
|
if (this.stream && nativeIdentity && this.stream.nativeIdentity !== nativeIdentity) {
|
|
1292
1313
|
if (!this.stream.nativeIdentity && !this.stream.published) {
|
|
1293
|
-
|
|
1294
|
-
if (!messageId) {
|
|
1295
|
-
this.stream = null;
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
this.stream.messageId = messageId;
|
|
1314
|
+
this.stream.messageId = this.messageIdForNativeIdentity(nativeIdentity);
|
|
1299
1315
|
this.stream.nativeIdentity = nativeIdentity;
|
|
1300
1316
|
} else {
|
|
1301
1317
|
this.flush();
|
|
@@ -1303,7 +1319,6 @@ export class OmpTimelineProjector {
|
|
|
1303
1319
|
}
|
|
1304
1320
|
}
|
|
1305
1321
|
const stream = this.stream ?? this.beginStream(message, turnId);
|
|
1306
|
-
if (!stream) return;
|
|
1307
1322
|
if (update?.contentIndex !== undefined) {
|
|
1308
1323
|
this.updateBlock(stream, message, update.contentIndex, update);
|
|
1309
1324
|
return;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { RpcInput, RpcOutput } from "@getpaseo/plugin";
|
|
2
|
+
import type { cancelOmpSubagent, steerOmpSubagent } from "../shared/subagents";
|
|
3
|
+
import { OmpRpcRequestRejectedError } from "./provider/omp-rpc-transport";
|
|
4
|
+
import { isOmpPublicError, OmpPublicError } from "./provider/security";
|
|
5
|
+
|
|
6
|
+
/** Subagent controls of one live OMP session; history-only sessions never register. */
|
|
7
|
+
export interface OmpSubagentControls {
|
|
8
|
+
/** Resolves `false` when the subagent is no longer running. */
|
|
9
|
+
cancelSubagent(handle: string): Promise<boolean>;
|
|
10
|
+
steerSubagent(handle: string, message: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class OmpSubagentNotRunningError extends OmpPublicError {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("This subagent is no longer running");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SubagentOperation = "cancel" | "steer";
|
|
20
|
+
|
|
21
|
+
const OPERATION_FAILURES: Record<SubagentOperation, string> = {
|
|
22
|
+
cancel: "OMP could not cancel the subagent",
|
|
23
|
+
steer: "OMP did not accept the message. The subagent may have finished or refused it",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// A timed-out request is not withdrawn from OMP, so its outcome is unknown rather than failed.
|
|
27
|
+
const OPERATION_TIMEOUTS: Record<SubagentOperation, string> = {
|
|
28
|
+
cancel: "OMP did not confirm the cancellation in time. Check the subagent before retrying",
|
|
29
|
+
steer: "OMP did not confirm delivery in time. The subagent may still receive the message",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Routes subagent controls to the OMP session that owns a Paseo agent. Keys are the agent ID the
|
|
34
|
+
* plugin's own `agent.session_open` hook injects, so a request can only reach that agent's
|
|
35
|
+
* runtime, and OMP then resolves the subagent only within that runtime's roster.
|
|
36
|
+
*/
|
|
37
|
+
export class OmpSubagentControlRegistry {
|
|
38
|
+
private readonly agents = new Map<string, { controls: OmpSubagentControls }>();
|
|
39
|
+
|
|
40
|
+
/** A newer session for the same agent replaces the older one; its removal is then a no-op. */
|
|
41
|
+
register(agentId: string, controls: OmpSubagentControls): () => void {
|
|
42
|
+
const entry = { controls };
|
|
43
|
+
this.agents.set(agentId, entry);
|
|
44
|
+
return () => {
|
|
45
|
+
if (this.agents.get(agentId) === entry) this.agents.delete(agentId);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get(agentId: string): OmpSubagentControls | undefined {
|
|
50
|
+
return this.agents.get(agentId)?.controls;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
clear(): void {
|
|
54
|
+
this.agents.clear();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function withPublicFailure<T>(operation: SubagentOperation, run: () => Promise<T>) {
|
|
59
|
+
try {
|
|
60
|
+
return await run();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (isOmpPublicError(error)) throw error;
|
|
63
|
+
if (error instanceof OmpRpcRequestRejectedError && error.code === "unsupported_command") {
|
|
64
|
+
throw new OmpPublicError("Update OMP to 18.4.9 or newer to control subagents from Paseo");
|
|
65
|
+
}
|
|
66
|
+
if (error instanceof Error && error.message === "OMP RPC request timed out") {
|
|
67
|
+
throw new OmpPublicError(OPERATION_TIMEOUTS[operation]);
|
|
68
|
+
}
|
|
69
|
+
// Native OMP error text is never forwarded; it can echo arbitrary subagent output.
|
|
70
|
+
throw new OmpPublicError(OPERATION_FAILURES[operation]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function controlsFor(registry: OmpSubagentControlRegistry, agentId: string): OmpSubagentControls {
|
|
75
|
+
const controls = registry.get(agentId);
|
|
76
|
+
if (!controls) throw new OmpPublicError("This agent has no running OMP Plugin session");
|
|
77
|
+
return controls;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function resolveCancelOmpSubagent(
|
|
81
|
+
input: RpcInput<typeof cancelOmpSubagent>,
|
|
82
|
+
registry: OmpSubagentControlRegistry,
|
|
83
|
+
): Promise<RpcOutput<typeof cancelOmpSubagent>> {
|
|
84
|
+
const controls = controlsFor(registry, input.agentId);
|
|
85
|
+
return {
|
|
86
|
+
cancelled: await withPublicFailure("cancel", () => controls.cancelSubagent(input.subagentId)),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function resolveSteerOmpSubagent(
|
|
91
|
+
input: RpcInput<typeof steerOmpSubagent>,
|
|
92
|
+
registry: OmpSubagentControlRegistry,
|
|
93
|
+
): Promise<RpcOutput<typeof steerOmpSubagent>> {
|
|
94
|
+
const controls = controlsFor(registry, input.agentId);
|
|
95
|
+
await withPublicFailure("steer", () => controls.steerSubagent(input.subagentId, input.message));
|
|
96
|
+
return { accepted: true };
|
|
97
|
+
}
|
|
@@ -3,9 +3,13 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
export const MAX_INHERITED_ENVIRONMENT_NAMES = 256;
|
|
5
5
|
export const environmentNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u);
|
|
6
|
+
export const DEFAULT_MAX_SESSIONS = 32;
|
|
7
|
+
// Every allowed session can open at once without reaching the 128 in-flight operations bound.
|
|
8
|
+
export const MAX_SESSIONS_UPPER_BOUND = 64;
|
|
6
9
|
|
|
7
10
|
export const providerLaunchSettingsSchema = z.object({
|
|
8
11
|
inheritEnv: z.array(environmentNameSchema).max(MAX_INHERITED_ENVIRONMENT_NAMES).default([]),
|
|
12
|
+
maxSessions: z.number().int().min(1).max(MAX_SESSIONS_UPPER_BOUND).default(DEFAULT_MAX_SESSIONS),
|
|
9
13
|
});
|
|
10
14
|
|
|
11
15
|
export type ProviderLaunchSettings = z.infer<typeof providerLaunchSettingsSchema>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const OMP_SUBAGENT_STEER_MAX_LENGTH = 16_384;
|
|
5
|
+
export const OMP_SUBAGENT_DESCRIPTION_MAX_LENGTH = 512;
|
|
6
|
+
|
|
7
|
+
const AgentIdSchema = z.string().min(1).max(256);
|
|
8
|
+
const SubagentIdSchema = z.string().min(1).max(256);
|
|
9
|
+
|
|
10
|
+
export const OmpSubagentStatusSchema = z.enum([
|
|
11
|
+
"pending",
|
|
12
|
+
"running",
|
|
13
|
+
"started",
|
|
14
|
+
"completed",
|
|
15
|
+
"failed",
|
|
16
|
+
"aborted",
|
|
17
|
+
]);
|
|
18
|
+
export type OmpSubagentStatus = z.infer<typeof OmpSubagentStatusSchema>;
|
|
19
|
+
|
|
20
|
+
export const cancelOmpSubagent = defineRpc({
|
|
21
|
+
name: "paseo-omp.cancel-subagent",
|
|
22
|
+
input: z.object({ agentId: AgentIdSchema, subagentId: SubagentIdSchema }),
|
|
23
|
+
/** `false` when the subagent had already finished; cancelling is idempotent. */
|
|
24
|
+
output: z.object({ cancelled: z.boolean() }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const steerOmpSubagent = defineRpc({
|
|
28
|
+
name: "paseo-omp.steer-subagent",
|
|
29
|
+
input: z.object({
|
|
30
|
+
agentId: AgentIdSchema,
|
|
31
|
+
subagentId: SubagentIdSchema,
|
|
32
|
+
message: z.string().trim().min(1).max(OMP_SUBAGENT_STEER_MAX_LENGTH),
|
|
33
|
+
}),
|
|
34
|
+
output: z.object({ accepted: z.literal(true) }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const OMP_SUBAGENT_STRIP_KIND = "omp-subagents";
|
|
38
|
+
export const MAX_OMP_SUBAGENT_STRIP_ENTRIES = 64;
|
|
39
|
+
|
|
40
|
+
/** The running subagents of one `task` call, rendered below its tool card. */
|
|
41
|
+
export const ompSubagentStripSchema = z.object({
|
|
42
|
+
subagents: z
|
|
43
|
+
.array(
|
|
44
|
+
z.object({
|
|
45
|
+
/** Opaque handle scoped to the agent's current OMP session, never the native OMP ID. */
|
|
46
|
+
id: SubagentIdSchema,
|
|
47
|
+
agent: z.string().min(1).max(256),
|
|
48
|
+
description: z.string().max(OMP_SUBAGENT_DESCRIPTION_MAX_LENGTH).nullable(),
|
|
49
|
+
/** Epoch milliseconds when the plugin last saw the subagent start running. */
|
|
50
|
+
runningSince: z.number().int().nonnegative(),
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
.max(MAX_OMP_SUBAGENT_STRIP_ENTRIES),
|
|
54
|
+
});
|
|
55
|
+
export type OmpSubagentStrip = z.infer<typeof ompSubagentStripSchema>;
|
|
56
|
+
|
|
57
|
+
/** OMP reaches a subagent only while its roster lists it as running or pending. */
|
|
58
|
+
export function isLiveOmpSubagentStatus(status: OmpSubagentStatus): boolean {
|
|
59
|
+
return status === "pending" || status === "running";
|
|
60
|
+
}
|