@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.
@@ -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.onActivityChange();
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.onActivityChange();
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.onActivityChange();
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.onActivityChange();
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.onActivityChange();
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.onActivityChange();
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.onActivityChange();
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 (event.type === "auto_compaction_start" || event.type === "compaction_start") {
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 (event.type === "auto_compaction_end" || event.type === "compaction_end") {
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 | null {
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 | undefined {
1284
+ private messageIdForNativeIdentity(nativeIdentity: string): string {
1265
1285
  const existing = this.turnNativeMessageIds.get(nativeIdentity);
1266
- if (existing) return existing;
1267
- if (
1268
- this.nativeIdentitySaturated ||
1269
- this.turnNativeMessageIds.size >= MAX_TURN_NATIVE_IDENTITIES
1270
- ) {
1271
- this.nativeIdentitySaturated = true;
1272
- return undefined;
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
- const messageId = this.messageIdForNativeIdentity(nativeIdentity);
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
+ }