paseo-dsh-direct 0.2.12 → 0.2.14
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/package.json +4 -4
- package/server/dsh/bridge.ts +1 -0
- package/server/dsh/host.ts +41 -0
- package/server/dsh/runtime-safety.ts +244 -0
- package/server/dsh/service.ts +46 -4
- package/server/provider.ts +155 -15
- package/server/timeline.ts +38 -15
- package/shared/bridge-protocol.ts +4 -0
package/OVERVIEW.md
CHANGED
|
@@ -14,6 +14,7 @@ Runs DeepSeek Harness (`dsh`) as a native Paseo agent provider. Paseo clients (d
|
|
|
14
14
|
- Create, resume, cancel and close sessions; model list and mode switching from the Paseo UI.
|
|
15
15
|
- Token usage and context-window fill in Paseo's usage meter.
|
|
16
16
|
- DSH branding (name and icon) in the provider list.
|
|
17
|
+
- On Paseo 0.14, answers the host's idle runtime cleanup query and refuses cleanup while supported DSH checks find pending work (agents, inboxes, maintenance, background jobs, subagent completion delivery, goals, reminders). After the host releases a persisted session runtime, the next message resumes the same native DSH session.
|
|
17
18
|
|
|
18
19
|
## Processes and environment
|
|
19
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-dsh-direct",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "DeepSeek Harness Direct Provider Plugin for Paseo",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"!server/generated-version.ts"
|
|
34
34
|
],
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@getpaseo/client": "0.
|
|
37
|
-
"@getpaseo/protocol": "0.
|
|
36
|
+
"@getpaseo/client": "0.14.0",
|
|
37
|
+
"@getpaseo/protocol": "0.14.0",
|
|
38
38
|
"react": "~19.1.0",
|
|
39
39
|
"react-native": ">=0.81.5",
|
|
40
40
|
"zod": "^4.4.3"
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@getpaseo/plugin": "0.
|
|
60
|
+
"@getpaseo/plugin": "0.14.0",
|
|
61
61
|
"@tanstack/react-query": "^5.105.0",
|
|
62
62
|
"@types/mdast": "^4.0.4",
|
|
63
63
|
"@types/node": "^24.0.0",
|
package/server/dsh/bridge.ts
CHANGED
package/server/dsh/host.ts
CHANGED
|
@@ -30,6 +30,10 @@ export interface HostAgent {
|
|
|
30
30
|
id: string;
|
|
31
31
|
ctx: HostContext;
|
|
32
32
|
session: HostSession;
|
|
33
|
+
inbox?: {
|
|
34
|
+
readonly nextTurn: readonly unknown[];
|
|
35
|
+
readonly nextStep: readonly unknown[];
|
|
36
|
+
};
|
|
33
37
|
options: ModelSelection;
|
|
34
38
|
status: "idle" | "running";
|
|
35
39
|
followup(message: unknown): void;
|
|
@@ -92,6 +96,8 @@ export interface HostServices {
|
|
|
92
96
|
};
|
|
93
97
|
agents: {
|
|
94
98
|
get(id: string): HostAgent | undefined;
|
|
99
|
+
list?(): HostAgent[];
|
|
100
|
+
isOwnedBy?(id: string, owner: HostAgent): boolean;
|
|
95
101
|
create(options: AgentOptions): Promise<HostHandle>;
|
|
96
102
|
resume(options: AgentOptions): Promise<HostHandle>;
|
|
97
103
|
};
|
|
@@ -199,6 +205,28 @@ export interface HostServices {
|
|
|
199
205
|
| undefined
|
|
200
206
|
>;
|
|
201
207
|
};
|
|
208
|
+
jobs?: {
|
|
209
|
+
list(
|
|
210
|
+
caller?: string,
|
|
211
|
+
): Array<{
|
|
212
|
+
id: string;
|
|
213
|
+
owner?: string;
|
|
214
|
+
status: "running" | "stopping" | "completed" | "killed" | "failed";
|
|
215
|
+
}>;
|
|
216
|
+
};
|
|
217
|
+
goals?: {
|
|
218
|
+
get(
|
|
219
|
+
agent: HostAgent,
|
|
220
|
+
):
|
|
221
|
+
| {
|
|
222
|
+
phase?: string;
|
|
223
|
+
activation?: string;
|
|
224
|
+
}
|
|
225
|
+
| undefined;
|
|
226
|
+
};
|
|
227
|
+
schedule?: {
|
|
228
|
+
list(request: { sessionId: string }): Promise<readonly unknown[]>;
|
|
229
|
+
};
|
|
202
230
|
userQuestions: object | undefined;
|
|
203
231
|
appReady: { onReady(listener: () => void): () => void } | undefined;
|
|
204
232
|
appExit: ((code: number) => void) | undefined;
|
|
@@ -216,6 +244,7 @@ export interface HostHelpers {
|
|
|
216
244
|
export type ApprovalOutcome =
|
|
217
245
|
"allowed-once" | "rejected" | "cancelled" | "unavailable";
|
|
218
246
|
export interface HostEvents {
|
|
247
|
+
"schedule/changed": () => void;
|
|
219
248
|
"session/created": (session: HostSession) => void;
|
|
220
249
|
"session/event": (session: HostSession, event: DshSessionEvent) => void;
|
|
221
250
|
"agent/assistant-stream": (payload: {
|
|
@@ -244,4 +273,16 @@ export interface HostEvents {
|
|
|
244
273
|
},
|
|
245
274
|
next: () => Promise<{ answers: QuestionAnswer[] }>,
|
|
246
275
|
) => Promise<{ answers: QuestionAnswer[] }>;
|
|
276
|
+
"subagent/start": (info: {
|
|
277
|
+
runId: string;
|
|
278
|
+
id: string;
|
|
279
|
+
provider?: string;
|
|
280
|
+
local?: boolean;
|
|
281
|
+
}) => void;
|
|
282
|
+
"subagent/end": (info: {
|
|
283
|
+
runId: string;
|
|
284
|
+
id: string;
|
|
285
|
+
provider?: string;
|
|
286
|
+
local?: boolean;
|
|
287
|
+
}) => void;
|
|
247
288
|
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { HostAgent, HostContext } from "./host.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks published subagent runs between `subagent/start` and `subagent/end`.
|
|
5
|
+
*
|
|
6
|
+
* The subagent manager emits `subagent/end` only after an activation's
|
|
7
|
+
* teardown finishes, its completion notice is delivered to the parent inbox,
|
|
8
|
+
* and parent ownership is released. While a run is in this table, stopping the
|
|
9
|
+
* bridge would lose an external child or a local child's settlement notice.
|
|
10
|
+
*/
|
|
11
|
+
export class SubagentRunTracker {
|
|
12
|
+
private readonly activeByRoot = new Map<string, Set<string>>();
|
|
13
|
+
private readonly rootByRun = new Map<string, string | undefined>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe inside a root session's `setup(agentCtx)` so direct delegations
|
|
17
|
+
* of that root always resolve to `rootId`, even for external backends whose
|
|
18
|
+
* child id is not in `ctx.agents` or `session/created`.
|
|
19
|
+
*/
|
|
20
|
+
observeRootScope(rootId: string, agentCtx: HostContext): void {
|
|
21
|
+
agentCtx.on("subagent/start", (info) => {
|
|
22
|
+
if (typeof info?.runId === "string" && info.runId) {
|
|
23
|
+
this.startRun(rootId, info.runId);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
agentCtx.on("subagent/end", (info) => {
|
|
27
|
+
if (typeof info?.runId === "string" && info.runId) {
|
|
28
|
+
this.endRun(info.runId);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Observe every process-level run, including external children whose parent
|
|
35
|
+
* is outside the bridge-owned session tree.
|
|
36
|
+
*/
|
|
37
|
+
observeHost(
|
|
38
|
+
ctx: HostContext,
|
|
39
|
+
resolveRoot: (childId: string) => string | undefined,
|
|
40
|
+
): Array<() => void> {
|
|
41
|
+
return [
|
|
42
|
+
ctx.on("subagent/start", (info) => {
|
|
43
|
+
if (typeof info?.runId !== "string" || !info.runId) return;
|
|
44
|
+
if (this.rootByRun.has(info.runId)) return;
|
|
45
|
+
const rootId =
|
|
46
|
+
typeof info.id === "string" ? resolveRoot(info.id) : undefined;
|
|
47
|
+
this.startRun(rootId, info.runId);
|
|
48
|
+
}),
|
|
49
|
+
ctx.on("subagent/end", (info) => {
|
|
50
|
+
if (typeof info?.runId === "string" && info.runId) {
|
|
51
|
+
this.endRun(info.runId);
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hasActiveRuns(rootId?: string): boolean {
|
|
58
|
+
return rootId === undefined
|
|
59
|
+
? this.rootByRun.size > 0
|
|
60
|
+
: (this.activeByRoot.get(rootId)?.size ?? 0) > 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clearRoot(rootId: string): void {
|
|
64
|
+
const runs = this.activeByRoot.get(rootId);
|
|
65
|
+
if (runs) {
|
|
66
|
+
for (const runId of runs) this.rootByRun.delete(runId);
|
|
67
|
+
this.activeByRoot.delete(rootId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clearAll(): void {
|
|
72
|
+
this.activeByRoot.clear();
|
|
73
|
+
this.rootByRun.clear();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private startRun(rootId: string | undefined, runId: string): void {
|
|
77
|
+
const existing = this.rootByRun.get(runId);
|
|
78
|
+
if (existing && existing !== rootId) {
|
|
79
|
+
this.activeByRoot.get(existing)?.delete(runId);
|
|
80
|
+
}
|
|
81
|
+
this.rootByRun.set(runId, rootId);
|
|
82
|
+
if (rootId === undefined) return;
|
|
83
|
+
let set = this.activeByRoot.get(rootId);
|
|
84
|
+
if (!set) {
|
|
85
|
+
set = new Set();
|
|
86
|
+
this.activeByRoot.set(rootId, set);
|
|
87
|
+
}
|
|
88
|
+
set.add(runId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private endRun(runId: string): void {
|
|
92
|
+
const rootId = this.rootByRun.get(runId);
|
|
93
|
+
this.rootByRun.delete(runId);
|
|
94
|
+
if (!rootId) return;
|
|
95
|
+
const set = this.activeByRoot.get(rootId);
|
|
96
|
+
if (!set) return;
|
|
97
|
+
set.delete(runId);
|
|
98
|
+
if (set.size === 0) this.activeByRoot.delete(rootId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface RuntimeSafetyInput {
|
|
103
|
+
ctx: HostContext;
|
|
104
|
+
sessionId: string;
|
|
105
|
+
rootAgent: HostAgent | undefined;
|
|
106
|
+
hasBridgeActivity: () => boolean;
|
|
107
|
+
hasActiveSubagentRuns: () => boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decide whether releasing this DSH session runtime loses in-flight work.
|
|
112
|
+
*
|
|
113
|
+
* This query is strictly read-only: it never cancels, disposes, mutates, or
|
|
114
|
+
* leaves behind uncancelled listeners. Any missing required inspection surface
|
|
115
|
+
* or thrown error fails closed (`false`).
|
|
116
|
+
*/
|
|
117
|
+
export async function canStopSessionRuntime(
|
|
118
|
+
input: RuntimeSafetyInput,
|
|
119
|
+
): Promise<boolean> {
|
|
120
|
+
let disposeScheduleObserver: (() => void) | undefined;
|
|
121
|
+
try {
|
|
122
|
+
if (input.hasBridgeActivity() || input.hasActiveSubagentRuns()) return false;
|
|
123
|
+
const root = input.rootAgent;
|
|
124
|
+
if (!root || String(root.id) !== input.sessionId) return false;
|
|
125
|
+
const agents = input.ctx.get("agents");
|
|
126
|
+
const jobs = input.ctx.get("jobs");
|
|
127
|
+
const goals = input.ctx.get("goals");
|
|
128
|
+
const schedule = input.ctx.get("schedule");
|
|
129
|
+
if (
|
|
130
|
+
!agents ||
|
|
131
|
+
typeof agents.get !== "function" ||
|
|
132
|
+
typeof agents.list !== "function" ||
|
|
133
|
+
!jobs ||
|
|
134
|
+
typeof jobs.list !== "function" ||
|
|
135
|
+
(goals && typeof goals.get !== "function") ||
|
|
136
|
+
(schedule && typeof schedule.list !== "function")
|
|
137
|
+
) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let scheduleChanged = false;
|
|
142
|
+
if (schedule) {
|
|
143
|
+
disposeScheduleObserver = input.ctx.on("schedule/changed", () => {
|
|
144
|
+
scheduleChanged = true;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Stopping the bridge exits the entire DSH process, including agents
|
|
148
|
+
// started by the profile or schedule runtime outside this session's tree.
|
|
149
|
+
const readAgents = (): HostAgent[] => {
|
|
150
|
+
if (agents.get(input.sessionId) !== root) return [];
|
|
151
|
+
const live = agents.list!();
|
|
152
|
+
return Array.isArray(live) && live.includes(root) ? live : [];
|
|
153
|
+
};
|
|
154
|
+
const liveAgents = readAgents();
|
|
155
|
+
if (liveAgents.length === 0 || !(await areAgentsQuiescent(liveAgents))) return false;
|
|
156
|
+
if (schedule) {
|
|
157
|
+
for (const agent of liveAgents) {
|
|
158
|
+
const active = await schedule.list({ sessionId: String(agent.id) });
|
|
159
|
+
if (!Array.isArray(active) || active.length > 0) return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Schedule reads yield to arbitrary runtime work. Probe maintenance again,
|
|
164
|
+
// then re-read every synchronous veto before returning the decision.
|
|
165
|
+
if (!(await areAgentsQuiescent(liveAgents))) return false;
|
|
166
|
+
const currentAgents = readAgents();
|
|
167
|
+
if (
|
|
168
|
+
scheduleChanged ||
|
|
169
|
+
input.hasBridgeActivity() ||
|
|
170
|
+
input.hasActiveSubagentRuns() ||
|
|
171
|
+
currentAgents.length !== liveAgents.length ||
|
|
172
|
+
currentAgents.some((agent) => !liveAgents.includes(agent))
|
|
173
|
+
) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
for (const agent of liveAgents) {
|
|
177
|
+
if (!isAgentSynchronouslyIdle(agent)) return false;
|
|
178
|
+
const goal = goals?.get(agent);
|
|
179
|
+
if (goal?.phase === "active" && goal.activation === "armed") return false;
|
|
180
|
+
}
|
|
181
|
+
// No caller filter: jobs can outlive their owner agent's registry entry.
|
|
182
|
+
const views = jobs.list();
|
|
183
|
+
if (
|
|
184
|
+
!Array.isArray(views) || views.some(
|
|
185
|
+
(job) => job?.status === "running" || job?.status === "stopping",
|
|
186
|
+
)
|
|
187
|
+
) return false;
|
|
188
|
+
return true;
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
} finally {
|
|
192
|
+
try {
|
|
193
|
+
disposeScheduleObserver?.();
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isAgentSynchronouslyIdle(agent: HostAgent): boolean {
|
|
201
|
+
if (agent.status !== "idle") return false;
|
|
202
|
+
const inbox = agent.inbox;
|
|
203
|
+
if (
|
|
204
|
+
!inbox ||
|
|
205
|
+
!Array.isArray(inbox.nextTurn) ||
|
|
206
|
+
!Array.isArray(inbox.nextStep)
|
|
207
|
+
) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return inbox.nextTurn.length === 0 && inbox.nextStep.length === 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Repeated safety queries during a long maintenance job must not attach another
|
|
214
|
+
// whenIdle continuation to the same unfinished activity on every request.
|
|
215
|
+
const idleObservations = new WeakMap<
|
|
216
|
+
HostAgent,
|
|
217
|
+
{ state: "pending" | "idle" | "failed" }
|
|
218
|
+
>();
|
|
219
|
+
|
|
220
|
+
async function areAgentsQuiescent(agents: readonly HostAgent[]): Promise<boolean> {
|
|
221
|
+
const observations = [];
|
|
222
|
+
for (const agent of agents) {
|
|
223
|
+
if (!isAgentSynchronouslyIdle(agent) || typeof agent.whenIdle !== "function") {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
let observation = idleObservations.get(agent);
|
|
227
|
+
if (!observation || observation.state !== "pending") {
|
|
228
|
+
observation = { state: "pending" };
|
|
229
|
+
idleObservations.set(agent, observation);
|
|
230
|
+
const current = observation;
|
|
231
|
+
void Promise.resolve(agent.whenIdle()).then(
|
|
232
|
+
() => { current.state = "idle"; },
|
|
233
|
+
() => { current.state = "failed"; },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
observations.push(observation);
|
|
237
|
+
}
|
|
238
|
+
// DSH's async whenIdle awaits activityDone and checks that its identity has
|
|
239
|
+
// not changed. Allow that resolved-promise path to finish without waiting for
|
|
240
|
+
// pending maintenance, timers, or attaching listeners to a long-lived race.
|
|
241
|
+
await Promise.resolve().then(() => undefined).then(() => undefined);
|
|
242
|
+
return observations.every((observation) => observation.state === "idle") &&
|
|
243
|
+
agents.every(isAgentSynchronouslyIdle);
|
|
244
|
+
}
|
package/server/dsh/service.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
SlashCommand,
|
|
20
20
|
} from "../../shared/bridge-protocol.js";
|
|
21
21
|
import { SubagentSettings } from "./subagent-settings.js";
|
|
22
|
+
import { canStopSessionRuntime, SubagentRunTracker } from "./runtime-safety.js";
|
|
22
23
|
import { slashLine } from "../../shared/bridge-protocol.js";
|
|
23
24
|
import type {
|
|
24
25
|
ApprovalOutcome,
|
|
@@ -147,6 +148,7 @@ export class BridgeService {
|
|
|
147
148
|
private stopTask?: Promise<{}>;
|
|
148
149
|
private readonly subagentSettings: SubagentSettings;
|
|
149
150
|
private subagentSettingsInitializationError?: Error;
|
|
151
|
+
private readonly subagentRuns = new SubagentRunTracker();
|
|
150
152
|
|
|
151
153
|
constructor(
|
|
152
154
|
private readonly ctx: HostContext,
|
|
@@ -157,11 +159,12 @@ export class BridgeService {
|
|
|
157
159
|
this.subagentSettings = new SubagentSettings(ctx, profile);
|
|
158
160
|
this.subscriptions.push(
|
|
159
161
|
ctx.on("session/created", (session) => {
|
|
160
|
-
if (session.header.parentSession)
|
|
162
|
+
if (session.header.parentSession) {
|
|
161
163
|
this.parent.set(
|
|
162
164
|
String(session.id),
|
|
163
165
|
String(session.header.parentSession),
|
|
164
166
|
);
|
|
167
|
+
}
|
|
165
168
|
}),
|
|
166
169
|
);
|
|
167
170
|
this.subscriptions.push(
|
|
@@ -237,6 +240,11 @@ export class BridgeService {
|
|
|
237
240
|
});
|
|
238
241
|
}),
|
|
239
242
|
);
|
|
243
|
+
this.subscriptions.push(
|
|
244
|
+
...this.subagentRuns.observeHost(ctx, (childId) =>
|
|
245
|
+
this.rootForId(childId),
|
|
246
|
+
),
|
|
247
|
+
);
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
/**
|
|
@@ -316,13 +324,19 @@ export class BridgeService {
|
|
|
316
324
|
}
|
|
317
325
|
|
|
318
326
|
private rootFor(agent: HostAgent | undefined): string | undefined {
|
|
319
|
-
|
|
327
|
+
return agent && this.rootForId(String(agent.id), agent);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private rootForId(
|
|
331
|
+
sessionId: string,
|
|
332
|
+
agent = this.ctx.get("agents")?.get(sessionId),
|
|
333
|
+
): string | undefined {
|
|
334
|
+
let id: string | undefined = sessionId;
|
|
320
335
|
const seen = new Set<string>();
|
|
321
336
|
while (id && !seen.has(id)) {
|
|
322
337
|
if (this.tracked.has(id)) return id;
|
|
323
338
|
seen.add(id);
|
|
324
|
-
id =
|
|
325
|
-
this.parent.get(id) ??
|
|
339
|
+
id = this.parent.get(id) ??
|
|
326
340
|
(id === agent?.id ? agent.session.header.parentSession : undefined);
|
|
327
341
|
}
|
|
328
342
|
return undefined;
|
|
@@ -581,6 +595,13 @@ export class BridgeService {
|
|
|
581
595
|
);
|
|
582
596
|
result = {};
|
|
583
597
|
break;
|
|
598
|
+
case "session.can_stop_runtime":
|
|
599
|
+
result = {
|
|
600
|
+
canStop: await this.canStopRuntime(
|
|
601
|
+
nonempty(params.sessionId, "sessionId"),
|
|
602
|
+
),
|
|
603
|
+
};
|
|
604
|
+
break;
|
|
584
605
|
case "session.close":
|
|
585
606
|
await this.close(nonempty(params.sessionId, "sessionId"));
|
|
586
607
|
result = {};
|
|
@@ -815,6 +836,7 @@ export class BridgeService {
|
|
|
815
836
|
this.tracked.delete(id);
|
|
816
837
|
this.preapproved.delete(id);
|
|
817
838
|
this.lastUsage.delete(id);
|
|
839
|
+
this.subagentRuns.clearRoot(id);
|
|
818
840
|
},
|
|
819
841
|
);
|
|
820
842
|
return task;
|
|
@@ -853,6 +875,7 @@ export class BridgeService {
|
|
|
853
875
|
assembled: undefined,
|
|
854
876
|
};
|
|
855
877
|
const setup = async (agentCtx: HostContext) => {
|
|
878
|
+
this.subagentRuns.observeRootScope(id, agentCtx);
|
|
856
879
|
this.helpers?.installModelSelection(agentCtx, modelRef);
|
|
857
880
|
await presets.mount(agentCtx, preset.id);
|
|
858
881
|
if (params.systemPrompt) {
|
|
@@ -950,6 +973,23 @@ export class BridgeService {
|
|
|
950
973
|
pending.finish({ answers: params.answers });
|
|
951
974
|
}
|
|
952
975
|
|
|
976
|
+
private canStopRuntime(sessionId: string): Promise<boolean> {
|
|
977
|
+
return canStopSessionRuntime({
|
|
978
|
+
ctx: this.ctx,
|
|
979
|
+
sessionId,
|
|
980
|
+
rootAgent: this.owned.get(sessionId)?.handle.agent,
|
|
981
|
+
hasBridgeActivity: () =>
|
|
982
|
+
this.stopping ||
|
|
983
|
+
this.opening.has(sessionId) ||
|
|
984
|
+
this.closing.has(sessionId) ||
|
|
985
|
+
this.commandAborts.has(sessionId) ||
|
|
986
|
+
[...this.interactions.values()].some(
|
|
987
|
+
(pending) => pending.request.sessionId === sessionId,
|
|
988
|
+
),
|
|
989
|
+
hasActiveSubagentRuns: () => this.subagentRuns.hasActiveRuns(),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
953
993
|
private close(id: string): Promise<void> {
|
|
954
994
|
const previous = this.closing.get(id);
|
|
955
995
|
if (previous) return previous;
|
|
@@ -964,6 +1004,7 @@ export class BridgeService {
|
|
|
964
1004
|
this.tracked.delete(id);
|
|
965
1005
|
this.preapproved.delete(id);
|
|
966
1006
|
this.lastUsage.delete(id);
|
|
1007
|
+
this.subagentRuns.clearRoot(id);
|
|
967
1008
|
})();
|
|
968
1009
|
this.closing.set(id, task);
|
|
969
1010
|
void task.finally(() => this.closing.delete(id)).catch(() => undefined);
|
|
@@ -978,6 +1019,7 @@ export class BridgeService {
|
|
|
978
1019
|
const results = await Promise.allSettled(
|
|
979
1020
|
[...this.owned.keys()].map((id) => this.close(id)),
|
|
980
1021
|
);
|
|
1022
|
+
this.subagentRuns.clearAll();
|
|
981
1023
|
for (const dispose of this.subscriptions.splice(0)) dispose();
|
|
982
1024
|
if (results.some((result) => result.status === "rejected"))
|
|
983
1025
|
throw new Error("One or more DSH sessions failed to close");
|
package/server/provider.ts
CHANGED
|
@@ -48,6 +48,7 @@ const CAPABILITIES = [
|
|
|
48
48
|
"prompt.command",
|
|
49
49
|
"prompt.image",
|
|
50
50
|
"prompt.steer",
|
|
51
|
+
"session.can_stop_runtime",
|
|
51
52
|
"session.configure",
|
|
52
53
|
"session.list",
|
|
53
54
|
"session.persistence",
|
|
@@ -57,6 +58,8 @@ const CAPABILITIES = [
|
|
|
57
58
|
] as const;
|
|
58
59
|
const PERMISSION_PRESET_SETTING = "permissionPreset";
|
|
59
60
|
const PLAN_MODE_SETTING = "planMode";
|
|
61
|
+
// Leave room for the host's ten-second safety-query deadline and IPC overhead.
|
|
62
|
+
const RUNTIME_SAFETY_TIMEOUT_MS = 5_000;
|
|
60
63
|
|
|
61
64
|
type DetectVersion = (
|
|
62
65
|
executable: string,
|
|
@@ -104,6 +107,11 @@ interface SessionState {
|
|
|
104
107
|
planMode: boolean;
|
|
105
108
|
pendingInteractions: Map<string, InteractionRequest>;
|
|
106
109
|
lane: Promise<void>;
|
|
110
|
+
/** Includes admitted input still waiting for its dispatch microtask or lane. */
|
|
111
|
+
pendingOperations: number;
|
|
112
|
+
/** Reject a safety snapshot if any input or native activity happened meanwhile. */
|
|
113
|
+
activityRevision: number;
|
|
114
|
+
ready: boolean;
|
|
107
115
|
closed: boolean;
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -188,7 +196,23 @@ function createConnection(
|
|
|
188
196
|
): ProviderConnection {
|
|
189
197
|
const listeners = new Set<(event: ProviderEvent) => void>();
|
|
190
198
|
const sessions = new Map<string, SessionState>();
|
|
199
|
+
const pendingOpens = new Set<Promise<void>>();
|
|
200
|
+
const openingBridges = new Set<DshBridge>();
|
|
201
|
+
const bridgeClosures = new WeakMap<DshBridge, Promise<void>>();
|
|
202
|
+
const cleanupErrors: unknown[] = [];
|
|
191
203
|
let closed = false;
|
|
204
|
+
let closePromise: Promise<void> | undefined;
|
|
205
|
+
const closeBridge = (bridge: DshBridge): Promise<void> => {
|
|
206
|
+
let closing = bridgeClosures.get(bridge);
|
|
207
|
+
if (!closing) {
|
|
208
|
+
closing = Promise.resolve().then(() => bridge.close()).catch((error) => {
|
|
209
|
+
cleanupErrors.push(error);
|
|
210
|
+
throw error;
|
|
211
|
+
});
|
|
212
|
+
bridgeClosures.set(bridge, closing);
|
|
213
|
+
}
|
|
214
|
+
return closing;
|
|
215
|
+
};
|
|
192
216
|
const emit = (event: ProviderEvent) => {
|
|
193
217
|
if (closed) return;
|
|
194
218
|
for (const listener of listeners) listener(event);
|
|
@@ -200,28 +224,50 @@ function createConnection(
|
|
|
200
224
|
async send(input) {
|
|
201
225
|
if (closed) throw new Error("Provider connection is closed");
|
|
202
226
|
validateAdmission(input, sessions, capabilities);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
const operationSession = input.type !== "session.can_stop_runtime" && "sessionId" in input
|
|
228
|
+
? sessions.get(input.sessionId)
|
|
229
|
+
: undefined;
|
|
230
|
+
if (operationSession) {
|
|
231
|
+
operationSession.pendingOperations += 1;
|
|
232
|
+
operationSession.activityRevision += 1;
|
|
233
|
+
}
|
|
234
|
+
const pending = Promise.resolve().then(async () => {
|
|
235
|
+
if (closed) return;
|
|
236
|
+
await dispatch(input, {
|
|
237
|
+
discoveryBridge, createBridge, sessions, emit, capabilities, detectVersion, launchEnv,
|
|
238
|
+
openingBridges, closeBridge, isClosed: () => closed,
|
|
239
|
+
});
|
|
240
|
+
}).catch((error) => failInput(input, error, emit)).finally(() => {
|
|
241
|
+
if (operationSession) operationSession.pendingOperations -= 1;
|
|
207
242
|
});
|
|
243
|
+
if (input.type === "session.open") {
|
|
244
|
+
pendingOpens.add(pending);
|
|
245
|
+
void pending.then(() => pendingOpens.delete(pending));
|
|
246
|
+
}
|
|
208
247
|
},
|
|
209
248
|
onEvent(listener) {
|
|
210
249
|
listeners.add(listener);
|
|
211
250
|
return () => listeners.delete(listener);
|
|
212
251
|
},
|
|
213
|
-
|
|
214
|
-
if (
|
|
252
|
+
close() {
|
|
253
|
+
if (closePromise) return closePromise;
|
|
215
254
|
closed = true;
|
|
216
|
-
const bridges = [discoveryBridge];
|
|
255
|
+
const bridges = new Set([discoveryBridge, ...openingBridges]);
|
|
217
256
|
for (const session of sessions.values()) {
|
|
218
257
|
session.closed = true;
|
|
219
258
|
session.releaseBridgeListeners.forEach((release) => release());
|
|
220
|
-
bridges.
|
|
259
|
+
bridges.add(session.bridge);
|
|
221
260
|
}
|
|
222
261
|
sessions.clear();
|
|
223
262
|
listeners.clear();
|
|
224
|
-
|
|
263
|
+
closePromise = (async () => {
|
|
264
|
+
await Promise.allSettled([
|
|
265
|
+
...Array.from(bridges, closeBridge),
|
|
266
|
+
...pendingOpens,
|
|
267
|
+
]);
|
|
268
|
+
if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Could not close DSH bridges");
|
|
269
|
+
})();
|
|
270
|
+
return closePromise;
|
|
225
271
|
},
|
|
226
272
|
};
|
|
227
273
|
}
|
|
@@ -236,6 +282,7 @@ function listenToSessionBridge(
|
|
|
236
282
|
return [
|
|
237
283
|
session.bridge.on("session.event", ({ sessionId, event }) => {
|
|
238
284
|
if (!isCurrentSession(sessionId)) return;
|
|
285
|
+
session.activityRevision += 1;
|
|
239
286
|
// The prompt path emits its own bubble with the clientMessageId.
|
|
240
287
|
if (event.type === "user/message" && session.dispatch?.kind === "prompt") return;
|
|
241
288
|
if (event.type === "turn/start") {
|
|
@@ -280,10 +327,12 @@ function listenToSessionBridge(
|
|
|
280
327
|
}),
|
|
281
328
|
session.bridge.on("session.stream", ({ sessionId, frame }) => {
|
|
282
329
|
if (!isCurrentSession(sessionId)) return;
|
|
330
|
+
session.activityRevision += 1;
|
|
283
331
|
for (const projected of session.projector.projectStream(frame)) emit(projected);
|
|
284
332
|
}),
|
|
285
333
|
session.bridge.on("session.status", ({ sessionId, status }) => {
|
|
286
334
|
if (!isCurrentSession(sessionId)) return;
|
|
335
|
+
session.activityRevision += 1;
|
|
287
336
|
if (status === "running") {
|
|
288
337
|
if (session.dispatch) session.dispatch.sawRunning = true;
|
|
289
338
|
return;
|
|
@@ -296,6 +345,7 @@ function listenToSessionBridge(
|
|
|
296
345
|
}),
|
|
297
346
|
session.bridge.on("session.usage", ({ sessionId, usage }) => {
|
|
298
347
|
if (!isCurrentSession(sessionId)) return;
|
|
348
|
+
session.activityRevision += 1;
|
|
299
349
|
emit({
|
|
300
350
|
type: "session.usage",
|
|
301
351
|
sessionId: session.paseoId,
|
|
@@ -305,6 +355,7 @@ function listenToSessionBridge(
|
|
|
305
355
|
}),
|
|
306
356
|
session.bridge.on("interaction.request", (interaction) => {
|
|
307
357
|
if (!isCurrentSession(interaction.sessionId)) return;
|
|
358
|
+
session.activityRevision += 1;
|
|
308
359
|
session.pendingInteractions.set(interaction.requestId, interaction);
|
|
309
360
|
emit({
|
|
310
361
|
type: "session.permission",
|
|
@@ -314,6 +365,7 @@ function listenToSessionBridge(
|
|
|
314
365
|
}),
|
|
315
366
|
session.bridge.on("interaction.closed", ({ requestId, sessionId }) => {
|
|
316
367
|
if (!isCurrentSession(sessionId) || !session.pendingInteractions.delete(requestId)) return;
|
|
368
|
+
session.activityRevision += 1;
|
|
317
369
|
emit({
|
|
318
370
|
type: "session.permission_resolved",
|
|
319
371
|
sessionId: session.paseoId,
|
|
@@ -363,6 +415,9 @@ interface DispatchState {
|
|
|
363
415
|
capabilities: readonly string[];
|
|
364
416
|
detectVersion: DetectVersion;
|
|
365
417
|
launchEnv: Readonly<Record<string, string>> | undefined;
|
|
418
|
+
openingBridges: Set<DshBridge>;
|
|
419
|
+
closeBridge(bridge: DshBridge): Promise<void>;
|
|
420
|
+
isClosed(): boolean;
|
|
366
421
|
}
|
|
367
422
|
|
|
368
423
|
async function dispatch(input: ProviderInput, state: DispatchState): Promise<void> {
|
|
@@ -392,6 +447,9 @@ async function dispatch(input: ProviderInput, state: DispatchState): Promise<voi
|
|
|
392
447
|
case "session.configure":
|
|
393
448
|
await enqueue(requireSession(state.sessions, input.sessionId), () => configureSession(input, state));
|
|
394
449
|
return;
|
|
450
|
+
case "session.can_stop_runtime":
|
|
451
|
+
await canStopRuntime(input, state);
|
|
452
|
+
return;
|
|
395
453
|
case "session.close":
|
|
396
454
|
await closeSession(input, state);
|
|
397
455
|
return;
|
|
@@ -452,8 +510,10 @@ async function openSession(
|
|
|
452
510
|
ephemeral: !input.config.persist,
|
|
453
511
|
mcpServers: bridgeMcpServers(input.config.mcpServers),
|
|
454
512
|
});
|
|
513
|
+
state.openingBridges.add(bridge);
|
|
455
514
|
let session: SessionState | undefined;
|
|
456
515
|
try {
|
|
516
|
+
if (state.isClosed()) return;
|
|
457
517
|
// Resume from DSH's durable model and preset. Paseo fills catalog defaults into
|
|
458
518
|
// open config, so forwarding them would accidentally overwrite native history.
|
|
459
519
|
const selection = nativeId
|
|
@@ -473,11 +533,13 @@ async function openSession(
|
|
|
473
533
|
? { preapprovedTools: preapprovedTools(input.config) }
|
|
474
534
|
: {}),
|
|
475
535
|
});
|
|
536
|
+
if (state.isClosed()) return;
|
|
476
537
|
const title = nativeId ? undefined : input.config.title?.trim();
|
|
477
538
|
const [commands] = await Promise.all([
|
|
478
539
|
fetchCommands(bridge, opened.sessionId),
|
|
479
540
|
title ? bridge.request("session.rename", { sessionId: opened.sessionId, title }) : undefined,
|
|
480
541
|
]);
|
|
542
|
+
if (state.isClosed()) return;
|
|
481
543
|
session = {
|
|
482
544
|
paseoId: input.sessionId,
|
|
483
545
|
nativeId: opened.sessionId,
|
|
@@ -493,6 +555,9 @@ async function openSession(
|
|
|
493
555
|
planMode: latestPlanMode(opened.events),
|
|
494
556
|
pendingInteractions: new Map(),
|
|
495
557
|
lane: Promise.resolve(),
|
|
558
|
+
pendingOperations: 0,
|
|
559
|
+
activityRevision: 0,
|
|
560
|
+
ready: false,
|
|
496
561
|
closed: false,
|
|
497
562
|
};
|
|
498
563
|
state.sessions.set(input.sessionId, session);
|
|
@@ -501,10 +566,18 @@ async function openSession(
|
|
|
501
566
|
await setPlanMode(session, true);
|
|
502
567
|
}
|
|
503
568
|
} catch (error) {
|
|
504
|
-
if (session)
|
|
505
|
-
|
|
569
|
+
if (session) {
|
|
570
|
+
session.closed = true;
|
|
571
|
+
session.releaseBridgeListeners.forEach((release) => release());
|
|
572
|
+
state.sessions.delete(input.sessionId);
|
|
573
|
+
}
|
|
574
|
+
await state.closeBridge(bridge);
|
|
506
575
|
throw error;
|
|
576
|
+
} finally {
|
|
577
|
+
state.openingBridges.delete(bridge);
|
|
578
|
+
if (state.isClosed()) await state.closeBridge(bridge);
|
|
507
579
|
}
|
|
580
|
+
if (state.isClosed()) return;
|
|
508
581
|
state.emit({
|
|
509
582
|
type: "session.opened",
|
|
510
583
|
requestId: input.requestId,
|
|
@@ -525,6 +598,8 @@ async function openSession(
|
|
|
525
598
|
session.projector.seed(session.open.events);
|
|
526
599
|
}
|
|
527
600
|
await warnOnDshVersionMismatch(session, state);
|
|
601
|
+
if (state.isClosed() || session.closed) return;
|
|
602
|
+
session.ready = true;
|
|
528
603
|
state.emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });
|
|
529
604
|
}
|
|
530
605
|
|
|
@@ -781,6 +856,54 @@ async function respondToPermission(
|
|
|
781
856
|
}
|
|
782
857
|
}
|
|
783
858
|
|
|
859
|
+
async function canStopRuntime(
|
|
860
|
+
input: Extract<ProviderInput, { type: "session.can_stop_runtime" }>,
|
|
861
|
+
state: DispatchState,
|
|
862
|
+
): Promise<void> {
|
|
863
|
+
const session = requireSession(state.sessions, input.sessionId);
|
|
864
|
+
if (!isProviderSessionIdle(session)) {
|
|
865
|
+
state.emit({
|
|
866
|
+
type: "session.can_stop_runtime",
|
|
867
|
+
requestId: input.requestId,
|
|
868
|
+
sessionId: input.sessionId,
|
|
869
|
+
canStop: false,
|
|
870
|
+
});
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const revision = session.activityRevision;
|
|
874
|
+
const result = await session.bridge.request(
|
|
875
|
+
"session.can_stop_runtime",
|
|
876
|
+
{ sessionId: session.nativeId },
|
|
877
|
+
RUNTIME_SAFETY_TIMEOUT_MS,
|
|
878
|
+
);
|
|
879
|
+
if (typeof result?.canStop !== "boolean") {
|
|
880
|
+
throw new Error("Invalid DSH runtime safety response");
|
|
881
|
+
}
|
|
882
|
+
const canStop =
|
|
883
|
+
result.canStop &&
|
|
884
|
+
state.sessions.get(input.sessionId) === session &&
|
|
885
|
+
session.activityRevision === revision &&
|
|
886
|
+
isProviderSessionIdle(session);
|
|
887
|
+
state.emit({
|
|
888
|
+
type: "session.can_stop_runtime",
|
|
889
|
+
requestId: input.requestId,
|
|
890
|
+
sessionId: input.sessionId,
|
|
891
|
+
canStop,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function isProviderSessionIdle(session: SessionState): boolean {
|
|
896
|
+
return (
|
|
897
|
+
session.ready &&
|
|
898
|
+
!session.closed &&
|
|
899
|
+
session.config.persist &&
|
|
900
|
+
!session.activeTurnId &&
|
|
901
|
+
!session.dispatch &&
|
|
902
|
+
session.pendingInteractions.size === 0 &&
|
|
903
|
+
session.pendingOperations === 0
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
784
907
|
async function closeSession(
|
|
785
908
|
input: Extract<ProviderInput, { type: "session.close" }>,
|
|
786
909
|
state: DispatchState,
|
|
@@ -816,6 +939,7 @@ function validateAdmission(
|
|
|
816
939
|
if (sessions.has(input.sessionId)) throw new Error(`Session already exists: ${input.sessionId}`);
|
|
817
940
|
return;
|
|
818
941
|
}
|
|
942
|
+
if (input.type === "session.can_stop_runtime") return;
|
|
819
943
|
if ("sessionId" in input && !sessions.has(input.sessionId)) {
|
|
820
944
|
throw new Error(`Unknown session: ${input.sessionId}`);
|
|
821
945
|
}
|
|
@@ -1085,12 +1209,28 @@ function questionAnswers(
|
|
|
1085
1209
|
return questions.map((question) => {
|
|
1086
1210
|
const key = question.header ?? question.id;
|
|
1087
1211
|
const raw = answers?.[key];
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1212
|
+
if (typeof raw !== "string" || raw.length === 0) return { id: question.id, selected: [] };
|
|
1213
|
+
const labels = (question.options ?? []).map((option) => option.label);
|
|
1214
|
+
if (!question.multiSelect) {
|
|
1215
|
+
return labels.includes(raw)
|
|
1216
|
+
? { id: question.id, selected: [raw] }
|
|
1217
|
+
: { id: question.id, selected: [], custom: raw };
|
|
1218
|
+
}
|
|
1219
|
+
// Paseo joins checked labels and the optional typed answer with ", ".
|
|
1220
|
+
// Match complete labels (longest first), keeping the remaining text intact.
|
|
1221
|
+
const candidates = labels.filter(Boolean).sort((left, right) => right.length - left.length);
|
|
1222
|
+
const selected: string[] = [];
|
|
1223
|
+
let custom = raw;
|
|
1224
|
+
while (custom.length > 0) {
|
|
1225
|
+
const label = candidates.find((value) =>
|
|
1226
|
+
!selected.includes(value) && (custom === value || custom.startsWith(`${value}, `)));
|
|
1227
|
+
if (label === undefined) break;
|
|
1228
|
+
selected.push(label);
|
|
1229
|
+
custom = custom === label ? "" : custom.slice(label.length + 2);
|
|
1230
|
+
}
|
|
1091
1231
|
return {
|
|
1092
1232
|
id: question.id,
|
|
1093
|
-
selected
|
|
1233
|
+
selected,
|
|
1094
1234
|
...(custom ? { custom } : {}),
|
|
1095
1235
|
};
|
|
1096
1236
|
});
|
package/server/timeline.ts
CHANGED
|
@@ -480,32 +480,55 @@ function timeline(
|
|
|
480
480
|
|
|
481
481
|
function toolDetail(name: string, args: JsonObject): ProviderToolCallDetail {
|
|
482
482
|
const normalized = name.toLowerCase();
|
|
483
|
-
|
|
483
|
+
|
|
484
|
+
// DSH's built-in tools are matched by exact name. Substring patterns get the
|
|
485
|
+
// common ones wrong (`todo_write` is not a file write, `read_image` is not a
|
|
486
|
+
// text read), and they would also claim unrelated MCP tools.
|
|
487
|
+
if (normalized === "bash" || normalized === "pwsh") {
|
|
488
|
+
// An empty command during streaming keeps the card from showing the raw
|
|
489
|
+
// half-parsed JSON; the final frame fills it in.
|
|
484
490
|
return {
|
|
485
491
|
type: "shell",
|
|
486
|
-
command: stringValue(args.command) ??
|
|
487
|
-
cwd: stringValue(args.
|
|
492
|
+
command: stringValue(args.command) ?? "",
|
|
493
|
+
cwd: stringValue(args.workdir),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (normalized === "read" || normalized === "read_image") {
|
|
497
|
+
return {
|
|
498
|
+
type: "read",
|
|
499
|
+
filePath: stringValue(args.file_path) ?? "",
|
|
500
|
+
offset: numberValue(args.offset),
|
|
501
|
+
limit: numberValue(args.limit),
|
|
488
502
|
};
|
|
489
503
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
504
|
+
if (normalized === "write") {
|
|
505
|
+
return {
|
|
506
|
+
type: "write",
|
|
507
|
+
filePath: stringValue(args.file_path) ?? "",
|
|
508
|
+
content: stringValue(args.content),
|
|
509
|
+
};
|
|
493
510
|
}
|
|
494
|
-
if (
|
|
511
|
+
if (normalized === "edit") {
|
|
495
512
|
return {
|
|
496
513
|
type: "edit",
|
|
497
|
-
filePath,
|
|
498
|
-
oldString: stringValue(args.
|
|
499
|
-
newString: stringValue(args.
|
|
500
|
-
unifiedDiff: stringValue(args.patch) ?? stringValue(args.diff),
|
|
514
|
+
filePath: stringValue(args.file_path) ?? "",
|
|
515
|
+
oldString: stringValue(args.old_string),
|
|
516
|
+
newString: stringValue(args.new_string),
|
|
501
517
|
};
|
|
502
518
|
}
|
|
503
|
-
if (
|
|
504
|
-
return { type: "
|
|
519
|
+
if (normalized === "grep") {
|
|
520
|
+
return { type: "search", query: stringValue(args.pattern) ?? "", toolName: "grep" };
|
|
505
521
|
}
|
|
506
|
-
if (
|
|
507
|
-
return { type: "search", query: stringValue(args.
|
|
522
|
+
if (normalized === "glob") {
|
|
523
|
+
return { type: "search", query: stringValue(args.pattern) ?? "", toolName: "glob" };
|
|
508
524
|
}
|
|
525
|
+
if (normalized === "web_fetch") {
|
|
526
|
+
return { type: "fetch", url: stringValue(args.url) ?? "" };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Everything else, including MCP tools, keeps a generic detail carrying the
|
|
530
|
+
// raw arguments. Guessing a card type from the name would render MCP tools as
|
|
531
|
+
// file operations.
|
|
509
532
|
return { type: "unknown", input: { raw: stringifyValue(args) }, output: {} };
|
|
510
533
|
}
|
|
511
534
|
|
|
@@ -204,6 +204,10 @@ export interface BridgeMethods {
|
|
|
204
204
|
result: {};
|
|
205
205
|
};
|
|
206
206
|
"session.cancel": { params: { sessionId: string }; result: {} };
|
|
207
|
+
"session.can_stop_runtime": {
|
|
208
|
+
params: { sessionId: string };
|
|
209
|
+
result: { canStop: boolean };
|
|
210
|
+
};
|
|
207
211
|
"session.close": { params: { sessionId: string }; result: {} };
|
|
208
212
|
"interaction.respond": {
|
|
209
213
|
params: {
|