@omercnet/paseo-omp 5.1.1-next.339.1 → 5.1.1-next.340.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/subagents-state.ts +108 -0
- package/client/subagents-strip.tsx +391 -0
- package/index.client.tsx +9 -0
- package/index.server.ts +16 -0
- package/package.json +1 -1
- package/server/package-version.ts +1 -1
- package/server/provider/connection.ts +16 -3
- package/server/provider/omp-rpc-protocol.ts +4 -2
- package/server/provider/omp-rpc.ts +25 -0
- package/server/provider/registration.ts +3 -0
- package/server/provider/session.ts +37 -2
- package/server/provider/subsessions.ts +109 -7
- package/server/subagents.ts +97 -0
- package/shared/subagents.ts +60 -0
package/OVERVIEW.md
CHANGED
|
@@ -11,6 +11,7 @@ OMP adds an agent provider for the OMP coding agent, plus an OMP sidebar and wor
|
|
|
11
11
|
- Edit OMP settings and project configuration for the default store or a named profile.
|
|
12
12
|
- Manage OMP's own plugins, and view quota history, memory, hub processes, session history and a support report.
|
|
13
13
|
- An **MCP** composer control runs OMP's MCP management in the current session, with authorization prompts shown in the chat.
|
|
14
|
+
- While OMP subagents run, a row under each `task` card in the conversation lets you steer one with a message or stop it after confirmation. It needs OMP 18.4.9 or newer.
|
|
14
15
|
- OMP agents can call Paseo's orchestration tools with the optional `serverId` argument and `list_hosts` to delegate work to other connected Paseo hosts.
|
|
15
16
|
|
|
16
17
|
## Reads and sends
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ paseo plugin install npm:@omercnet/paseo-omp@<version>
|
|
|
31
31
|
paseo plugin ls paseo-omp
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Open the **OMP** sidebar to review the provider-profile contract, choose which composer pills appear, browse and edit native scalar settings, manage OMP-native plugins, verify runtime health, and generate a bounded support report under **Help**. The same tabs are available in the workspace OMP panel, except for the global Composer preference tab. Then create an agent and select **OMP Plugin**. Its optional **MCP** composer control runs OMP-native management commands in the current session; setup questions and authorization stay in that chat timeline, where OAuth can open in a workspace-scoped Paseo Browser or on the current device. Composer pill preferences are shared by clients connected to the same host; normal use requires no other plugin-specific settings.
|
|
34
|
+
Open the **OMP** sidebar to review the provider-profile contract, choose which composer pills appear, browse and edit native scalar settings, manage OMP-native plugins, verify runtime health, and generate a bounded support report under **Help**. The same tabs are available in the workspace OMP panel, except for the global Composer preference tab. Then create an agent and select **OMP Plugin**. Its optional **MCP** composer control runs OMP-native management commands in the current session; setup questions and authorization stay in that chat timeline, where OAuth can open in a workspace-scoped Paseo Browser or on the current device. While OMP subagents run, a row under each `task` card in the conversation lets you steer or stop one of them; this needs OMP 18.4.9 or newer. Composer pill preferences are shared by clients connected to the same host; normal use requires no other plugin-specific settings.
|
|
35
35
|
|
|
36
36
|
- [Install, update, rollback, and local development](docs/installation.md)
|
|
37
37
|
- [Configuration and every provider option](docs/configuration.md)
|
|
@@ -84,7 +84,7 @@ Scoring is deliberately mechanical so releases remain comparable:
|
|
|
84
84
|
| `session.revert.both` | **0%** | Not advertised because OMP cannot atomically rewind conversation and workspace files. |
|
|
85
85
|
| `session.revert.conversation` | **100%** | Native branch-based conversation rewind, replay deduplication, ownership transfer, and failure quarantine. |
|
|
86
86
|
| `session.revert.files` | **0%** | Not advertised because OMP does not expose a native file-only rewind contract. |
|
|
87
|
-
| `session.subsession` | **100%** | Direct and nested OMP subagents become Paseo child sessions with lifecycle, ancestry, progress, and replay. |
|
|
87
|
+
| `session.subsession` | **100%** | Direct and nested OMP subagents become Paseo child sessions with lifecycle, ancestry, progress, and replay. Paseo's child sessions are read-only, so the provider also publishes a plugin timeline row under each `task` card that steers or stops one running child through OMP's `steer_subagent` and `cancel_subagent` (OMP 18.4.9 or newer) without stopping the parent turn. |
|
|
88
88
|
| `session.unarchive` | **0%** | Not advertised; there is no native OMP archive state to reverse. |
|
|
89
89
|
| `permission` | **100%** | Typed tool permissions when negotiated, with a bounded generic interaction fallback for OMP 18.1.15. Allow, deny, cancel, timeout, and interruption are covered. |
|
|
90
90
|
| `permission.tool_policy` | **0%** | Not advertised. Exact MCP preapproval policy cannot be preserved through OMP `set_host_tools`, so the plugin fails closed instead of broadening access. |
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { OmpSubagentStrip } from "../shared/subagents";
|
|
2
|
+
|
|
3
|
+
export type OmpSubagentRow = OmpSubagentStrip["subagents"][number];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One row of a subagent strip at a time owns the stop confirmation, the steering draft, or the
|
|
7
|
+
* pending request. The state keeps that row as last listed, so its draft stays visible after the
|
|
8
|
+
* subagent leaves the strip.
|
|
9
|
+
*/
|
|
10
|
+
export type SubagentStripState =
|
|
11
|
+
| { kind: "idle" }
|
|
12
|
+
| { kind: "confirming-stop"; subagent: OmpSubagentRow }
|
|
13
|
+
| { kind: "steering"; subagent: OmpSubagentRow; draft: string }
|
|
14
|
+
| { kind: "stopping"; subagent: OmpSubagentRow }
|
|
15
|
+
| { kind: "sending"; subagent: OmpSubagentRow; draft: string };
|
|
16
|
+
|
|
17
|
+
export type SubagentStripAction =
|
|
18
|
+
| { type: "request-stop"; subagent: OmpSubagentRow }
|
|
19
|
+
| { type: "start-steer"; subagent: OmpSubagentRow }
|
|
20
|
+
| { type: "edit-draft"; draft: string }
|
|
21
|
+
| { type: "dismiss" }
|
|
22
|
+
| { type: "submit" }
|
|
23
|
+
| { type: "settled" }
|
|
24
|
+
| { type: "failed" };
|
|
25
|
+
|
|
26
|
+
type PendingState = Extract<SubagentStripState, { kind: "stopping" | "sending" }>;
|
|
27
|
+
|
|
28
|
+
export const INITIAL_SUBAGENT_STRIP_STATE: SubagentStripState = { kind: "idle" };
|
|
29
|
+
|
|
30
|
+
export function isSendableDraft(draft: string): boolean {
|
|
31
|
+
return draft.trim().length > 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isSubagentRequestPending(state: SubagentStripState): state is PendingState {
|
|
35
|
+
return state.kind === "stopping" || state.kind === "sending";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The subagent a row's controls belong to while it owns the strip, or `null`. */
|
|
39
|
+
export function activeSubagent(state: SubagentStripState): OmpSubagentRow | null {
|
|
40
|
+
return state.kind === "idle" ? null : state.subagent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The request a submit starts from this state, or `null` when it starts none. A submit while a
|
|
45
|
+
* request is pending starts nothing, so a repeated press cannot send a second steering message.
|
|
46
|
+
*/
|
|
47
|
+
export function requestedSubmission(state: SubagentStripState): PendingState | null {
|
|
48
|
+
if (isSubagentRequestPending(state)) return null;
|
|
49
|
+
const next = subagentStripReducer(state, { type: "submit" });
|
|
50
|
+
return isSubagentRequestPending(next) ? next : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function subagentStripReducer(
|
|
54
|
+
state: SubagentStripState,
|
|
55
|
+
action: SubagentStripAction,
|
|
56
|
+
): SubagentStripState {
|
|
57
|
+
if (isSubagentRequestPending(state)) return settle(state, action);
|
|
58
|
+
switch (action.type) {
|
|
59
|
+
case "request-stop":
|
|
60
|
+
return { kind: "confirming-stop", subagent: action.subagent };
|
|
61
|
+
case "start-steer":
|
|
62
|
+
return { kind: "steering", subagent: action.subagent, draft: "" };
|
|
63
|
+
case "edit-draft":
|
|
64
|
+
return state.kind === "steering" ? { ...state, draft: action.draft } : state;
|
|
65
|
+
case "dismiss":
|
|
66
|
+
return INITIAL_SUBAGENT_STRIP_STATE;
|
|
67
|
+
case "submit":
|
|
68
|
+
if (state.kind === "confirming-stop") return { kind: "stopping", subagent: state.subagent };
|
|
69
|
+
if (state.kind === "steering" && isSendableDraft(state.draft)) {
|
|
70
|
+
return { kind: "sending", subagent: state.subagent, draft: state.draft };
|
|
71
|
+
}
|
|
72
|
+
return state;
|
|
73
|
+
default:
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A failed send reopens its draft for another try; every other outcome closes the row. */
|
|
79
|
+
function settle(state: PendingState, action: SubagentStripAction): SubagentStripState {
|
|
80
|
+
if (action.type === "settled") return INITIAL_SUBAGENT_STRIP_STATE;
|
|
81
|
+
if (action.type !== "failed") return state;
|
|
82
|
+
return state.kind === "sending"
|
|
83
|
+
? { kind: "steering", subagent: state.subagent, draft: state.draft }
|
|
84
|
+
: INITIAL_SUBAGENT_STRIP_STATE;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function stopOutcomeMessage(cancelled: boolean): string {
|
|
88
|
+
return cancelled ? "Subagent stopped" : "The subagent had already finished";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The host's RPC error keeps the handler's text in `userMessage`, without transport details. */
|
|
92
|
+
export function subagentRequestError(cause: unknown): string {
|
|
93
|
+
if (cause instanceof Error) {
|
|
94
|
+
const userMessage: unknown = Reflect.get(cause, "userMessage");
|
|
95
|
+
return typeof userMessage === "string" && userMessage ? userMessage : cause.message;
|
|
96
|
+
}
|
|
97
|
+
return "The OMP subagent request failed";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Compact elapsed time, such as `42s`, `3m`, or `1h 5m`. */
|
|
101
|
+
export function formatElapsed(milliseconds: number): string {
|
|
102
|
+
const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
|
|
103
|
+
if (seconds < 60) return `${seconds}s`;
|
|
104
|
+
const minutes = Math.floor(seconds / 60);
|
|
105
|
+
if (minutes < 60) return `${minutes}m`;
|
|
106
|
+
const hours = Math.floor(minutes / 60);
|
|
107
|
+
return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PluginHostProps,
|
|
3
|
+
type PluginTimelineItemProps,
|
|
4
|
+
useRpc,
|
|
5
|
+
} from "@getpaseo/plugin/client";
|
|
6
|
+
import { Icon, TextInput, useToast } from "@getpaseo/plugin/client/ui";
|
|
7
|
+
import { useEffect, useMemo, useReducer, useState } from "react";
|
|
8
|
+
import { Pressable, Text, View } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
cancelOmpSubagent,
|
|
11
|
+
OMP_SUBAGENT_STEER_MAX_LENGTH,
|
|
12
|
+
type OmpSubagentStrip as OmpSubagentStripData,
|
|
13
|
+
steerOmpSubagent,
|
|
14
|
+
} from "../shared/subagents";
|
|
15
|
+
import {
|
|
16
|
+
activeSubagent,
|
|
17
|
+
formatElapsed,
|
|
18
|
+
INITIAL_SUBAGENT_STRIP_STATE,
|
|
19
|
+
isSendableDraft,
|
|
20
|
+
isSubagentRequestPending,
|
|
21
|
+
type OmpSubagentRow,
|
|
22
|
+
requestedSubmission,
|
|
23
|
+
type SubagentStripAction,
|
|
24
|
+
type SubagentStripState,
|
|
25
|
+
stopOutcomeMessage,
|
|
26
|
+
subagentRequestError,
|
|
27
|
+
subagentStripReducer,
|
|
28
|
+
} from "./subagents-state";
|
|
29
|
+
|
|
30
|
+
const ROW_HEIGHT = 28;
|
|
31
|
+
|
|
32
|
+
function createStyles(theme: PluginHostProps["theme"], compact: boolean) {
|
|
33
|
+
return {
|
|
34
|
+
root: { gap: 2, paddingVertical: 2 },
|
|
35
|
+
row: {
|
|
36
|
+
flexDirection: "row" as const,
|
|
37
|
+
alignItems: "center" as const,
|
|
38
|
+
gap: 8,
|
|
39
|
+
height: ROW_HEIGHT,
|
|
40
|
+
paddingLeft: compact ? 2 : 4,
|
|
41
|
+
},
|
|
42
|
+
dot: { width: 6, height: 6, borderRadius: 3, backgroundColor: theme.colors.accent },
|
|
43
|
+
dotFinished: { backgroundColor: theme.colors.foregroundMuted },
|
|
44
|
+
agent: { color: theme.colors.foreground, fontSize: 13, flexShrink: 0 },
|
|
45
|
+
description: { color: theme.colors.foregroundMuted, fontSize: 13, flexShrink: 1, minWidth: 0 },
|
|
46
|
+
elapsed: {
|
|
47
|
+
color: theme.colors.foregroundMuted,
|
|
48
|
+
fontSize: 12,
|
|
49
|
+
fontVariant: ["tabular-nums" as const],
|
|
50
|
+
},
|
|
51
|
+
actions: { flexDirection: "row" as const, alignItems: "center" as const, gap: 2 },
|
|
52
|
+
iconButton: {
|
|
53
|
+
width: 24,
|
|
54
|
+
height: 24,
|
|
55
|
+
borderRadius: 6,
|
|
56
|
+
alignItems: "center" as const,
|
|
57
|
+
justifyContent: "center" as const,
|
|
58
|
+
},
|
|
59
|
+
textButton: {
|
|
60
|
+
height: 24,
|
|
61
|
+
paddingHorizontal: 8,
|
|
62
|
+
borderRadius: 6,
|
|
63
|
+
justifyContent: "center" as const,
|
|
64
|
+
},
|
|
65
|
+
hovered: { backgroundColor: theme.colors.surface2 },
|
|
66
|
+
disabled: { opacity: 0.5 },
|
|
67
|
+
stopLabel: { color: theme.colors.statusDanger, fontSize: 12 },
|
|
68
|
+
keepLabel: { color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
69
|
+
tooltip: {
|
|
70
|
+
position: "absolute" as const,
|
|
71
|
+
bottom: 28,
|
|
72
|
+
right: 0,
|
|
73
|
+
paddingHorizontal: 6,
|
|
74
|
+
paddingVertical: 2,
|
|
75
|
+
borderRadius: 6,
|
|
76
|
+
backgroundColor: theme.colors.surface2,
|
|
77
|
+
borderWidth: 1,
|
|
78
|
+
borderColor: theme.colors.border,
|
|
79
|
+
},
|
|
80
|
+
tooltipText: { color: theme.colors.foreground, fontSize: 11 },
|
|
81
|
+
input: {
|
|
82
|
+
flexBasis: 360,
|
|
83
|
+
flexShrink: 1,
|
|
84
|
+
minWidth: 0,
|
|
85
|
+
height: 24,
|
|
86
|
+
paddingHorizontal: 8,
|
|
87
|
+
borderRadius: 6,
|
|
88
|
+
borderWidth: 1,
|
|
89
|
+
borderColor: theme.colors.border,
|
|
90
|
+
backgroundColor: theme.colors.surface1,
|
|
91
|
+
color: theme.colors.foreground,
|
|
92
|
+
fontSize: 13,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type Styles = ReturnType<typeof createStyles>;
|
|
98
|
+
|
|
99
|
+
/** Re-renders once per second while `active`, for elapsed times. */
|
|
100
|
+
function useSecondTick(active: boolean): number {
|
|
101
|
+
const [now, setNow] = useState(() => Date.now());
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!active) return;
|
|
104
|
+
const timer = setInterval(() => setNow(Date.now()), 1_000);
|
|
105
|
+
return () => clearInterval(timer);
|
|
106
|
+
}, [active]);
|
|
107
|
+
return now;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Running OMP subagents of one `task` call, published by the provider below its tool card. Paseo
|
|
112
|
+
* shows provider subagents read-only, so the plugin adds steer and stop here.
|
|
113
|
+
*/
|
|
114
|
+
export function OmpSubagentStrip({
|
|
115
|
+
agentId,
|
|
116
|
+
item,
|
|
117
|
+
theme,
|
|
118
|
+
layout,
|
|
119
|
+
}: PluginTimelineItemProps<OmpSubagentStripData>) {
|
|
120
|
+
const cancel = useRpc(cancelOmpSubagent);
|
|
121
|
+
const steer = useRpc(steerOmpSubagent);
|
|
122
|
+
const toast = useToast();
|
|
123
|
+
const styles = useMemo(() => createStyles(theme, layout.compact), [theme, layout.compact]);
|
|
124
|
+
const [state, dispatch] = useReducer(subagentStripReducer, INITIAL_SUBAGENT_STRIP_STATE);
|
|
125
|
+
const listed = item.data.subagents;
|
|
126
|
+
const active = activeSubagent(state);
|
|
127
|
+
const unlisted = active && !listed.some((row) => row.id === active.id) ? active : null;
|
|
128
|
+
const now = useSecondTick(listed.length > 0);
|
|
129
|
+
|
|
130
|
+
async function submit(): Promise<void> {
|
|
131
|
+
const request = requestedSubmission(state);
|
|
132
|
+
dispatch({ type: "submit" });
|
|
133
|
+
if (!request) return;
|
|
134
|
+
try {
|
|
135
|
+
if (request.kind === "stopping") {
|
|
136
|
+
const result = await cancel({ agentId, subagentId: request.subagent.id });
|
|
137
|
+
toast.show(stopOutcomeMessage(result.cancelled), {
|
|
138
|
+
variant: result.cancelled ? "success" : "default",
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
await steer({ agentId, subagentId: request.subagent.id, message: request.draft });
|
|
142
|
+
toast.show("Message sent to the subagent", { variant: "success" });
|
|
143
|
+
}
|
|
144
|
+
dispatch({ type: "settled" });
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
toast.error(subagentRequestError(cause));
|
|
147
|
+
dispatch({ type: "failed" });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (listed.length === 0 && !unlisted) return null;
|
|
152
|
+
return (
|
|
153
|
+
<View style={styles.root}>
|
|
154
|
+
{listed.map((row) => (
|
|
155
|
+
<SubagentRow
|
|
156
|
+
key={row.id}
|
|
157
|
+
row={row}
|
|
158
|
+
listed
|
|
159
|
+
now={now}
|
|
160
|
+
state={state}
|
|
161
|
+
styles={styles}
|
|
162
|
+
theme={theme}
|
|
163
|
+
dispatch={dispatch}
|
|
164
|
+
submit={submit}
|
|
165
|
+
/>
|
|
166
|
+
))}
|
|
167
|
+
{unlisted ? (
|
|
168
|
+
<SubagentRow
|
|
169
|
+
row={unlisted}
|
|
170
|
+
listed={false}
|
|
171
|
+
now={now}
|
|
172
|
+
state={state}
|
|
173
|
+
styles={styles}
|
|
174
|
+
theme={theme}
|
|
175
|
+
dispatch={dispatch}
|
|
176
|
+
submit={submit}
|
|
177
|
+
/>
|
|
178
|
+
) : null}
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface RowProps {
|
|
184
|
+
row: OmpSubagentRow;
|
|
185
|
+
/** False keeps the row that owns a draft visible after its subagent leaves the strip. */
|
|
186
|
+
listed: boolean;
|
|
187
|
+
now: number;
|
|
188
|
+
state: SubagentStripState;
|
|
189
|
+
styles: Styles;
|
|
190
|
+
theme: PluginHostProps["theme"];
|
|
191
|
+
dispatch: (action: SubagentStripAction) => void;
|
|
192
|
+
submit(): Promise<void>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function SubagentRow(props: RowProps) {
|
|
196
|
+
const { row, listed, now, state, styles } = props;
|
|
197
|
+
const owner = activeSubagent(state)?.id === row.id ? state : null;
|
|
198
|
+
const steering = owner?.kind === "steering" || owner?.kind === "sending";
|
|
199
|
+
return (
|
|
200
|
+
<View style={styles.row}>
|
|
201
|
+
<View style={[styles.dot, listed ? null : styles.dotFinished]} />
|
|
202
|
+
<Text numberOfLines={1} style={styles.agent}>
|
|
203
|
+
{row.agent}
|
|
204
|
+
</Text>
|
|
205
|
+
{steering ? (
|
|
206
|
+
<SteerControls {...props} />
|
|
207
|
+
) : (
|
|
208
|
+
<>
|
|
209
|
+
<Text numberOfLines={1} style={styles.description}>
|
|
210
|
+
{row.description ?? ""}
|
|
211
|
+
</Text>
|
|
212
|
+
{owner?.kind === "confirming-stop" || owner?.kind === "stopping" ? (
|
|
213
|
+
<StopConfirmation {...props} />
|
|
214
|
+
) : (
|
|
215
|
+
<>
|
|
216
|
+
<Text style={styles.elapsed}>
|
|
217
|
+
{listed ? formatElapsed(now - row.runningSince) : "finished"}
|
|
218
|
+
</Text>
|
|
219
|
+
{listed ? <RowActions {...props} /> : null}
|
|
220
|
+
</>
|
|
221
|
+
)}
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
</View>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function RowActions({ row, state, styles, theme, dispatch }: RowProps) {
|
|
229
|
+
const pending = isSubagentRequestPending(state);
|
|
230
|
+
return (
|
|
231
|
+
<View style={styles.actions}>
|
|
232
|
+
<IconButton
|
|
233
|
+
icon="MessageSquare"
|
|
234
|
+
label="Steer: send this subagent a message at its next step"
|
|
235
|
+
disabled={pending}
|
|
236
|
+
styles={styles}
|
|
237
|
+
theme={theme}
|
|
238
|
+
onPress={() => dispatch({ type: "start-steer", subagent: row })}
|
|
239
|
+
/>
|
|
240
|
+
<IconButton
|
|
241
|
+
icon="CircleStop"
|
|
242
|
+
label="Stop this subagent and its nested subagents; the parent keeps running"
|
|
243
|
+
disabled={pending}
|
|
244
|
+
styles={styles}
|
|
245
|
+
theme={theme}
|
|
246
|
+
onPress={() => dispatch({ type: "request-stop", subagent: row })}
|
|
247
|
+
/>
|
|
248
|
+
</View>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function StopConfirmation({ state, styles, dispatch, submit }: RowProps) {
|
|
253
|
+
const stopping = state.kind === "stopping";
|
|
254
|
+
return (
|
|
255
|
+
<View style={styles.actions}>
|
|
256
|
+
<TextButton
|
|
257
|
+
label={stopping ? "Stopping..." : "Stop"}
|
|
258
|
+
accessibilityLabel="Confirm stopping this subagent"
|
|
259
|
+
labelStyle={styles.stopLabel}
|
|
260
|
+
disabled={stopping}
|
|
261
|
+
styles={styles}
|
|
262
|
+
onPress={() => void submit()}
|
|
263
|
+
/>
|
|
264
|
+
<TextButton
|
|
265
|
+
label="Keep"
|
|
266
|
+
accessibilityLabel="Keep this subagent running"
|
|
267
|
+
labelStyle={styles.keepLabel}
|
|
268
|
+
disabled={stopping}
|
|
269
|
+
styles={styles}
|
|
270
|
+
onPress={() => dispatch({ type: "dismiss" })}
|
|
271
|
+
/>
|
|
272
|
+
</View>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function SteerControls({ row, state, styles, theme, dispatch, submit }: RowProps) {
|
|
277
|
+
const sending = state.kind === "sending";
|
|
278
|
+
const draft = state.kind === "steering" || state.kind === "sending" ? state.draft : "";
|
|
279
|
+
return (
|
|
280
|
+
<>
|
|
281
|
+
<TextInput
|
|
282
|
+
accessibilityLabel={`Message for ${row.agent}`}
|
|
283
|
+
autoFocus
|
|
284
|
+
editable={!sending}
|
|
285
|
+
maxLength={OMP_SUBAGENT_STEER_MAX_LENGTH}
|
|
286
|
+
onChangeText={(text) => dispatch({ type: "edit-draft", draft: text })}
|
|
287
|
+
onKeyPress={(event) => {
|
|
288
|
+
if (event.nativeEvent.key === "Escape") dispatch({ type: "dismiss" });
|
|
289
|
+
}}
|
|
290
|
+
onSubmitEditing={() => void submit()}
|
|
291
|
+
placeholder="Message the subagent"
|
|
292
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
293
|
+
style={styles.input}
|
|
294
|
+
value={draft}
|
|
295
|
+
/>
|
|
296
|
+
<View style={styles.actions}>
|
|
297
|
+
<IconButton
|
|
298
|
+
icon="SendHorizontal"
|
|
299
|
+
label="Send to the subagent (Enter)"
|
|
300
|
+
disabled={sending || !isSendableDraft(draft)}
|
|
301
|
+
styles={styles}
|
|
302
|
+
theme={theme}
|
|
303
|
+
onPress={() => void submit()}
|
|
304
|
+
/>
|
|
305
|
+
<IconButton
|
|
306
|
+
icon="X"
|
|
307
|
+
label="Close (Esc)"
|
|
308
|
+
disabled={sending}
|
|
309
|
+
styles={styles}
|
|
310
|
+
theme={theme}
|
|
311
|
+
onPress={() => dispatch({ type: "dismiss" })}
|
|
312
|
+
/>
|
|
313
|
+
</View>
|
|
314
|
+
</>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function IconButton({
|
|
319
|
+
icon,
|
|
320
|
+
label,
|
|
321
|
+
disabled,
|
|
322
|
+
styles,
|
|
323
|
+
theme,
|
|
324
|
+
onPress,
|
|
325
|
+
}: {
|
|
326
|
+
icon: string;
|
|
327
|
+
label: string;
|
|
328
|
+
disabled: boolean;
|
|
329
|
+
styles: Styles;
|
|
330
|
+
theme: PluginHostProps["theme"];
|
|
331
|
+
onPress(): void;
|
|
332
|
+
}) {
|
|
333
|
+
const [hovered, setHovered] = useState(false);
|
|
334
|
+
return (
|
|
335
|
+
<View>
|
|
336
|
+
<Pressable
|
|
337
|
+
accessibilityRole="button"
|
|
338
|
+
accessibilityLabel={label}
|
|
339
|
+
disabled={disabled}
|
|
340
|
+
onHoverIn={() => setHovered(true)}
|
|
341
|
+
onHoverOut={() => setHovered(false)}
|
|
342
|
+
onPress={onPress}
|
|
343
|
+
style={[
|
|
344
|
+
styles.iconButton,
|
|
345
|
+
hovered && !disabled ? styles.hovered : null,
|
|
346
|
+
disabled ? styles.disabled : null,
|
|
347
|
+
]}
|
|
348
|
+
>
|
|
349
|
+
<Icon name={icon} size={14} color={theme.colors.foregroundMuted} />
|
|
350
|
+
</Pressable>
|
|
351
|
+
{hovered && !disabled ? (
|
|
352
|
+
<View pointerEvents="none" style={styles.tooltip}>
|
|
353
|
+
<Text numberOfLines={1} style={styles.tooltipText}>
|
|
354
|
+
{label}
|
|
355
|
+
</Text>
|
|
356
|
+
</View>
|
|
357
|
+
) : null}
|
|
358
|
+
</View>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function TextButton({
|
|
363
|
+
label,
|
|
364
|
+
accessibilityLabel,
|
|
365
|
+
labelStyle,
|
|
366
|
+
disabled,
|
|
367
|
+
styles,
|
|
368
|
+
onPress,
|
|
369
|
+
}: {
|
|
370
|
+
label: string;
|
|
371
|
+
accessibilityLabel: string;
|
|
372
|
+
labelStyle: Styles["stopLabel"];
|
|
373
|
+
disabled: boolean;
|
|
374
|
+
styles: Styles;
|
|
375
|
+
onPress(): void;
|
|
376
|
+
}) {
|
|
377
|
+
const [hovered, setHovered] = useState(false);
|
|
378
|
+
return (
|
|
379
|
+
<Pressable
|
|
380
|
+
accessibilityRole="button"
|
|
381
|
+
accessibilityLabel={accessibilityLabel}
|
|
382
|
+
disabled={disabled}
|
|
383
|
+
onHoverIn={() => setHovered(true)}
|
|
384
|
+
onHoverOut={() => setHovered(false)}
|
|
385
|
+
onPress={onPress}
|
|
386
|
+
style={[styles.textButton, hovered && !disabled ? styles.hovered : null]}
|
|
387
|
+
>
|
|
388
|
+
<Text style={labelStyle}>{label}</Text>
|
|
389
|
+
</Pressable>
|
|
390
|
+
);
|
|
391
|
+
}
|
package/index.client.tsx
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
HUB_SIDEBAR_ITEM_ID,
|
|
39
39
|
loadHubSnapshot,
|
|
40
40
|
} from "./client/sidebar-compat";
|
|
41
|
+
import { OmpSubagentStrip } from "./client/subagents-strip";
|
|
41
42
|
import {
|
|
42
43
|
type ComposerPillSettings,
|
|
43
44
|
composerPillSettings,
|
|
@@ -48,6 +49,7 @@ import { OMP_MCP_AUTH_TIMELINE_KIND, ompMcpAuthorizationTimelineSchema } from ".
|
|
|
48
49
|
import { type OmpStore, storeForProvider } from "./shared/omp-store";
|
|
49
50
|
import { ompImageTimelineSchema, transformOmpImageToolItem } from "./shared/provider-image";
|
|
50
51
|
import { listOmpQuotas } from "./shared/quota";
|
|
52
|
+
import { OMP_SUBAGENT_STRIP_KIND, ompSubagentStripSchema } from "./shared/subagents";
|
|
51
53
|
|
|
52
54
|
const PAGE_LIMIT = 200;
|
|
53
55
|
const MAX_PAGES = 10;
|
|
@@ -237,6 +239,12 @@ export default function contribute(client: PluginClientContext) {
|
|
|
237
239
|
schema: ompMcpAuthorizationTimelineSchema,
|
|
238
240
|
Component: OmpMcpAuthorizationCard,
|
|
239
241
|
});
|
|
242
|
+
const removeSubagentStripRenderer = client.addTimelineRenderer({
|
|
243
|
+
kind: OMP_SUBAGENT_STRIP_KIND,
|
|
244
|
+
version: 1,
|
|
245
|
+
schema: ompSubagentStripSchema,
|
|
246
|
+
Component: OmpSubagentStrip,
|
|
247
|
+
});
|
|
240
248
|
const removeImageTransformer = client.addTimelineTransformer({
|
|
241
249
|
id: "omp-images",
|
|
242
250
|
query: { itemType: "tool_call" },
|
|
@@ -544,6 +552,7 @@ export default function contribute(client: PluginClientContext) {
|
|
|
544
552
|
pills.clear();
|
|
545
553
|
removeImageRenderer();
|
|
546
554
|
removeMcpAuthorizationRenderer();
|
|
555
|
+
removeSubagentStripRenderer();
|
|
547
556
|
removeImageTransformer();
|
|
548
557
|
removeOpenConfig();
|
|
549
558
|
removeConfigEntry();
|
package/index.server.ts
CHANGED
|
@@ -29,6 +29,11 @@ import { OmpSessionLimit } from "./server/provider/session-limit";
|
|
|
29
29
|
import { resolveGetOmpProviderHealth } from "./server/provider-diagnostics";
|
|
30
30
|
import { resolveListOmpQuotas } from "./server/quota";
|
|
31
31
|
import { resolveListOmpSessions } from "./server/sessions";
|
|
32
|
+
import {
|
|
33
|
+
OmpSubagentControlRegistry,
|
|
34
|
+
resolveCancelOmpSubagent,
|
|
35
|
+
resolveSteerOmpSubagent,
|
|
36
|
+
} from "./server/subagents";
|
|
32
37
|
import { resolveGetOmpSupportReport } from "./server/support-diagnostics";
|
|
33
38
|
import { composerPillSettings } from "./shared/composer-pill-settings";
|
|
34
39
|
import { listHubProcesses, tailHubLog } from "./shared/hub";
|
|
@@ -48,6 +53,7 @@ import { getOmpProviderHealth } from "./shared/provider-diagnostics";
|
|
|
48
53
|
import { providerLaunchSettings } from "./shared/provider-launch-settings";
|
|
49
54
|
import { listOmpQuotas } from "./shared/quota";
|
|
50
55
|
import { listOmpSessions } from "./shared/sessions";
|
|
56
|
+
import { cancelOmpSubagent, steerOmpSubagent } from "./shared/subagents";
|
|
51
57
|
import { getOmpSupportReport } from "./shared/support-diagnostics";
|
|
52
58
|
|
|
53
59
|
function scoped<T extends { store?: OmpStore }, R>(handler: (input: T) => R) {
|
|
@@ -68,6 +74,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
68
74
|
return current.status === "ready" ? current.values.maxSessions : undefined;
|
|
69
75
|
});
|
|
70
76
|
const removeSessionLimitListener = launchSettings.subscribe(() => sessionLimit.refresh());
|
|
77
|
+
const subagentControlRegistry = new OmpSubagentControlRegistry();
|
|
71
78
|
const protocolViolations = new OmpProtocolViolationCollector();
|
|
72
79
|
const operationalFailures = new OmpOperationalFailureCollector();
|
|
73
80
|
const profiles = discoverOmpProfilesSync();
|
|
@@ -77,6 +84,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
77
84
|
createProfileOmpProvider(profile, {
|
|
78
85
|
browserAuthorizationRegistry,
|
|
79
86
|
sessionLimit,
|
|
87
|
+
subagentControlRegistry,
|
|
80
88
|
reportProtocolViolation: protocolViolations.report,
|
|
81
89
|
reportOperationalFailure: operationalFailures.report,
|
|
82
90
|
resolveHostInheritEnv,
|
|
@@ -104,6 +112,12 @@ export default function contribute(server: PluginServerContext) {
|
|
|
104
112
|
server.handle(openOmpMcpAuthorizationInPaseoBrowser, (input) =>
|
|
105
113
|
resolveOpenOmpMcpAuthorizationInPaseoBrowser(input, browserAuthorizationRegistry),
|
|
106
114
|
);
|
|
115
|
+
server.handle(cancelOmpSubagent, (input) =>
|
|
116
|
+
resolveCancelOmpSubagent(input, subagentControlRegistry),
|
|
117
|
+
);
|
|
118
|
+
server.handle(steerOmpSubagent, (input) =>
|
|
119
|
+
resolveSteerOmpSubagent(input, subagentControlRegistry),
|
|
120
|
+
);
|
|
107
121
|
const removeSessionOpenHook = server.before("agent.session_open", ({ request }) => {
|
|
108
122
|
if (request.provider !== "omp-plugin" && !request.provider.startsWith("omp-plugin-")) return;
|
|
109
123
|
return withOmpSessionOpenEnv(request);
|
|
@@ -112,6 +126,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
112
126
|
createOmpProvider({
|
|
113
127
|
browserAuthorizationRegistry,
|
|
114
128
|
sessionLimit,
|
|
129
|
+
subagentControlRegistry,
|
|
115
130
|
reportProtocolViolation: protocolViolations.report,
|
|
116
131
|
reportOperationalFailure: operationalFailures.report,
|
|
117
132
|
resolveHostInheritEnv,
|
|
@@ -120,6 +135,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
120
135
|
return () => {
|
|
121
136
|
browserAuthorizationRegistry.clear();
|
|
122
137
|
removeSessionLimitListener();
|
|
138
|
+
subagentControlRegistry.clear();
|
|
123
139
|
removeSessionOpenHook();
|
|
124
140
|
};
|
|
125
141
|
}
|
package/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Release Please keeps this bundle-safe constant aligned with package.json. */
|
|
2
|
-
export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.340.1"; // x-release-please-version
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
OmpOperationalFailure,
|
|
14
14
|
OmpOperationalFailureReporter,
|
|
15
15
|
} from "../operational-failure-diagnostics";
|
|
16
|
+
import type { OmpSubagentControlRegistry } from "../subagents";
|
|
16
17
|
import { discoverOmpCatalog } from "./catalog";
|
|
17
18
|
import { normalizeOmpCatalogOptions } from "./config-normalization";
|
|
18
19
|
import type { OmpMcpConnector } from "./host-tools";
|
|
@@ -724,6 +725,7 @@ export function createOmpConnection(
|
|
|
724
725
|
reportOperationalFailure: OmpOperationalFailureReporter = () => {},
|
|
725
726
|
resolveHostInheritEnv?: () => Promise<readonly string[]>,
|
|
726
727
|
sessionLimit = new OmpSessionLimit(),
|
|
728
|
+
subagentControlRegistry?: OmpSubagentControlRegistry,
|
|
727
729
|
): ProviderConnection {
|
|
728
730
|
const errorDetails = (error: unknown, fallback: string): { message: string } => {
|
|
729
731
|
if (isOmpPublicError(error)) return { message: error.message };
|
|
@@ -758,6 +760,7 @@ export function createOmpConnection(
|
|
|
758
760
|
session: OmpOpenedSession;
|
|
759
761
|
nativeSessionId?: string;
|
|
760
762
|
removeBrowserAuthorization?: () => void;
|
|
763
|
+
removeSubagentControls?: () => void;
|
|
761
764
|
}
|
|
762
765
|
>();
|
|
763
766
|
const opening = new Map<
|
|
@@ -791,6 +794,7 @@ export function createOmpConnection(
|
|
|
791
794
|
if (slot?.token !== token) return false;
|
|
792
795
|
sessions.delete(sessionId);
|
|
793
796
|
slot.removeBrowserAuthorization?.();
|
|
797
|
+
slot.removeSubagentControls?.();
|
|
794
798
|
return true;
|
|
795
799
|
};
|
|
796
800
|
|
|
@@ -1033,12 +1037,12 @@ export function createOmpConnection(
|
|
|
1033
1037
|
nativeReservations.release(nativeSessionId, token);
|
|
1034
1038
|
return;
|
|
1035
1039
|
}
|
|
1036
|
-
const
|
|
1040
|
+
const agentId = input.config.env.PASEO_AGENT_ID?.trim() || input.sessionId;
|
|
1037
1041
|
// A history session has no live Paseo browser tools to authorize.
|
|
1038
1042
|
const browserAuthorization = historyOnly
|
|
1039
1043
|
? undefined
|
|
1040
1044
|
: browserAuthorizationRegistry?.register(
|
|
1041
|
-
|
|
1045
|
+
agentId,
|
|
1042
1046
|
session.openPaseoBrowser.bind(session),
|
|
1043
1047
|
);
|
|
1044
1048
|
session.setBrowserAuthorizationIssuer(browserAuthorization?.issue ?? null);
|
|
@@ -1048,11 +1052,17 @@ export function createOmpConnection(
|
|
|
1048
1052
|
browserAuthorization.remove();
|
|
1049
1053
|
}
|
|
1050
1054
|
: undefined;
|
|
1055
|
+
// A history session has no runtime, so it has no subagents to control.
|
|
1056
|
+
const removeSubagentControls =
|
|
1057
|
+
session instanceof OmpProviderSession
|
|
1058
|
+
? subagentControlRegistry?.register(agentId, session)
|
|
1059
|
+
: undefined;
|
|
1051
1060
|
sessions.set(input.sessionId, {
|
|
1052
1061
|
token,
|
|
1053
1062
|
session,
|
|
1054
1063
|
nativeSessionId,
|
|
1055
1064
|
...(removeBrowserAuthorization ? { removeBrowserAuthorization } : {}),
|
|
1065
|
+
...(removeSubagentControls ? { removeSubagentControls } : {}),
|
|
1056
1066
|
});
|
|
1057
1067
|
await session.publishOpened(input.requestId);
|
|
1058
1068
|
if (
|
|
@@ -1204,7 +1214,10 @@ export function createOmpConnection(
|
|
|
1204
1214
|
shutdown.abort(shutdownReason);
|
|
1205
1215
|
for (const { controller } of opening.values()) controller.abort(shutdownReason);
|
|
1206
1216
|
for (const { session } of sessions.values()) session.beginConnectionShutdown();
|
|
1207
|
-
for (const slot of sessions.values())
|
|
1217
|
+
for (const slot of sessions.values()) {
|
|
1218
|
+
slot.removeBrowserAuthorization?.();
|
|
1219
|
+
slot.removeSubagentControls?.();
|
|
1220
|
+
}
|
|
1208
1221
|
const failures: unknown[] = [];
|
|
1209
1222
|
const operationBatch = [...activeOperations];
|
|
1210
1223
|
const operationResults = await Promise.allSettled(operationBatch);
|
|
@@ -144,7 +144,7 @@ export const OMP_RPC_COMMAND_POLICIES = {
|
|
|
144
144
|
abort_and_restore_queue: "unsupported",
|
|
145
145
|
btw: "unsupported",
|
|
146
146
|
btw_cancel: "unsupported",
|
|
147
|
-
cancel_subagent: "
|
|
147
|
+
cancel_subagent: "implemented",
|
|
148
148
|
fork: "unsupported",
|
|
149
149
|
get_btw_history: "unsupported",
|
|
150
150
|
get_logout_accounts: "unsupported",
|
|
@@ -166,7 +166,7 @@ export const OMP_RPC_COMMAND_POLICIES = {
|
|
|
166
166
|
set_ask_dialog: "implemented",
|
|
167
167
|
set_cache_warming: "unsupported",
|
|
168
168
|
set_slow_mode: "unsupported",
|
|
169
|
-
steer_subagent: "
|
|
169
|
+
steer_subagent: "implemented",
|
|
170
170
|
} as const;
|
|
171
171
|
export const OMP_RPC_RESPONSE_POLICIES = OMP_RPC_COMMAND_POLICIES;
|
|
172
172
|
export const OMP_PROTOCOL_VIOLATION_CATEGORIES = [
|
|
@@ -2001,6 +2001,8 @@ export const OmpSubagentsResultSchema = z.object({
|
|
|
2001
2001
|
)
|
|
2002
2002
|
.max(1_024),
|
|
2003
2003
|
});
|
|
2004
|
+
export const OmpCancelSubagentResultSchema = z.object({ cancelled: z.boolean() });
|
|
2005
|
+
export const OmpSteerSubagentResultSchema = z.object({}).strict().optional();
|
|
2004
2006
|
export const OmpSubagentMessagesResultSchema = z.object({
|
|
2005
2007
|
sessionFile: boundedString(MAX_PATH_LENGTH),
|
|
2006
2008
|
fromByte: z.number().int().nonnegative(),
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
OmpBranchMessagesResultSchema,
|
|
17
17
|
type OmpBranchResult,
|
|
18
18
|
OmpBranchResultSchema,
|
|
19
|
+
OmpCancelSubagentResultSchema,
|
|
19
20
|
type OmpCompactionResult,
|
|
20
21
|
OmpCompactionResultSchema,
|
|
21
22
|
type OmpExtensionUiResponse,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
OmpSessionStateSchema,
|
|
42
43
|
type OmpSessionStats,
|
|
43
44
|
OmpSessionStatsSchema,
|
|
45
|
+
OmpSteerSubagentResultSchema,
|
|
44
46
|
type OmpSubagentMessagesResult,
|
|
45
47
|
OmpSubagentMessagesResultSchema,
|
|
46
48
|
type OmpSubagentSnapshot,
|
|
@@ -105,6 +107,9 @@ export interface OmpRuntimeSession {
|
|
|
105
107
|
subagentId?: string;
|
|
106
108
|
sessionFile?: string;
|
|
107
109
|
}): Promise<OmpSubagentMessagesResult>;
|
|
110
|
+
/** Resolves `false` when OMP no longer lists the subagent as running for this session. */
|
|
111
|
+
cancelSubagent(subagentId: string): Promise<boolean>;
|
|
112
|
+
steerSubagent(subagentId: string, message: string): Promise<void>;
|
|
108
113
|
prompt(
|
|
109
114
|
message: string,
|
|
110
115
|
images?: readonly OmpImage[],
|
|
@@ -466,6 +471,26 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
466
471
|
);
|
|
467
472
|
}
|
|
468
473
|
|
|
474
|
+
async cancelSubagent(subagentId: string): Promise<boolean> {
|
|
475
|
+
const safeId = validateBoundedText(subagentId, "subagent identifier", MAX_ID_LENGTH);
|
|
476
|
+
return OmpCancelSubagentResultSchema.parse(
|
|
477
|
+
await this.process.request({ type: "cancel_subagent", subagentId: safeId }),
|
|
478
|
+
).cancelled;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async steerSubagent(subagentId: string, message: string): Promise<void> {
|
|
482
|
+
const safeId = validateBoundedText(subagentId, "subagent identifier", MAX_ID_LENGTH);
|
|
483
|
+
const safeMessage = validateBoundedText(message, "subagent steering message", MAX_TEXT_LENGTH);
|
|
484
|
+
if (!safeMessage.trim()) throw new Error("Invalid OMP subagent steering message");
|
|
485
|
+
OmpSteerSubagentResultSchema.parse(
|
|
486
|
+
await this.process.request({
|
|
487
|
+
type: "steer_subagent",
|
|
488
|
+
subagentId: safeId,
|
|
489
|
+
message: safeMessage,
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
469
494
|
async getBranchMessages(): Promise<Array<{ entryId: string; text: string }>> {
|
|
470
495
|
const result = OmpBranchMessagesResultSchema.parse(
|
|
471
496
|
await this.process.request({ type: "get_branch_messages" }),
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
|
|
6
6
|
import type { OmpOperationalFailureReporter } from "../operational-failure-diagnostics";
|
|
7
7
|
import { probeOmpAvailability } from "../provider-diagnostics";
|
|
8
|
+
import type { OmpSubagentControlRegistry } from "../subagents";
|
|
8
9
|
import { mergeOmpInheritedEnvironmentNames } from "./config-normalization";
|
|
9
10
|
import { createOmpConnection, OmpNativeSessionReservations } from "./connection";
|
|
10
11
|
import type { OmpMcpConnector } from "./host-tools";
|
|
@@ -131,6 +132,7 @@ export interface OmpProviderOptions {
|
|
|
131
132
|
timeoutMs: number | undefined,
|
|
132
133
|
) => Promise<ProviderAvailabilityCompat>;
|
|
133
134
|
resolveHostInheritEnv?: () => Promise<readonly string[]>;
|
|
135
|
+
subagentControlRegistry?: OmpSubagentControlRegistry;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
function stableJson(value: unknown): string {
|
|
@@ -235,6 +237,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
|
|
|
235
237
|
options.reportOperationalFailure,
|
|
236
238
|
options.resolveHostInheritEnv,
|
|
237
239
|
sessionLimit,
|
|
240
|
+
options.subagentControlRegistry,
|
|
238
241
|
);
|
|
239
242
|
},
|
|
240
243
|
};
|
|
@@ -6,10 +6,12 @@ import type {
|
|
|
6
6
|
ProviderInput,
|
|
7
7
|
ProviderSessionConfig,
|
|
8
8
|
} from "@getpaseo/plugin/server/provider";
|
|
9
|
+
import { isLiveOmpSubagentStatus } from "../../shared/subagents";
|
|
9
10
|
import type {
|
|
10
11
|
OmpOperationalFailure,
|
|
11
12
|
OmpOperationalFailureReporter,
|
|
12
13
|
} from "../operational-failure-diagnostics";
|
|
14
|
+
import { type OmpSubagentControls, OmpSubagentNotRunningError } from "../subagents";
|
|
13
15
|
import {
|
|
14
16
|
mapOmpModels,
|
|
15
17
|
nativeOmpModelId,
|
|
@@ -84,7 +86,7 @@ import {
|
|
|
84
86
|
unknownTerminalOutcomeError,
|
|
85
87
|
} from "./session-terminal";
|
|
86
88
|
import { OmpSessionUsage } from "./session-usage";
|
|
87
|
-
import { OmpSubsessionProjector } from "./subsessions";
|
|
89
|
+
import { OmpSubsessionProjector, ompSubagentHandle } from "./subsessions";
|
|
88
90
|
import {
|
|
89
91
|
defaultOmpTimelineScheduler,
|
|
90
92
|
OmpTimelineProjector,
|
|
@@ -298,7 +300,7 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
|
|
|
298
300
|
throw new AggregateError(failures, "OMP session initialization cleanup failed");
|
|
299
301
|
}
|
|
300
302
|
}
|
|
301
|
-
export class OmpProviderSession implements OmpOpenedSession {
|
|
303
|
+
export class OmpProviderSession implements OmpOpenedSession, OmpSubagentControls {
|
|
302
304
|
readonly id: string;
|
|
303
305
|
readonly cwd: string;
|
|
304
306
|
|
|
@@ -465,6 +467,36 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
465
467
|
this.projector.setBrowserAuthorizationIssuer(issue);
|
|
466
468
|
}
|
|
467
469
|
|
|
470
|
+
async cancelSubagent(handle: string): Promise<boolean> {
|
|
471
|
+
const target = await this.liveSubagent(handle);
|
|
472
|
+
return target ? await target.runtime.cancelSubagent(target.nativeId) : false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async steerSubagent(handle: string, message: string): Promise<void> {
|
|
476
|
+
const target = await this.liveSubagent(handle);
|
|
477
|
+
if (!target) throw new OmpSubagentNotRunningError();
|
|
478
|
+
await target.runtime.steerSubagent(target.nativeId, message);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The native ID and runtime of a subagent this session's current runtime lists as running, or
|
|
483
|
+
* `null`. OMP repeats the check; this scopes the request to this session and answers exactly.
|
|
484
|
+
*/
|
|
485
|
+
private async liveSubagent(
|
|
486
|
+
handle: string,
|
|
487
|
+
): Promise<{ runtime: OmpRuntimeSession; nativeId: string } | null> {
|
|
488
|
+
if (this.closed || this.runtimeDead) return null;
|
|
489
|
+
const runtime = this.runtime;
|
|
490
|
+
const snapshots = await runtime.getSubagents();
|
|
491
|
+
if (runtime !== this.runtime || this.closed || this.runtimeDead) return null;
|
|
492
|
+
const live = snapshots.find(
|
|
493
|
+
(snapshot) =>
|
|
494
|
+
ompSubagentHandle(this.id, snapshot.id) === handle &&
|
|
495
|
+
isLiveOmpSubagentStatus(snapshot.status),
|
|
496
|
+
);
|
|
497
|
+
return live ? { runtime, nativeId: live.id } : null;
|
|
498
|
+
}
|
|
499
|
+
|
|
468
500
|
private readonly imageMaterializer = new OmpImageMaterializer();
|
|
469
501
|
static async open(
|
|
470
502
|
input: SessionOpenInput,
|
|
@@ -2658,6 +2690,9 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2658
2690
|
});
|
|
2659
2691
|
throw error;
|
|
2660
2692
|
}
|
|
2693
|
+
if (event.type === "tool_execution_start" && event.toolName === "task") {
|
|
2694
|
+
this.subsessions?.anchorRootTask(event.toolCallId);
|
|
2695
|
+
}
|
|
2661
2696
|
if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
|
|
2662
2697
|
}
|
|
2663
2698
|
|
|
@@ -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(
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|