@morlay/dsh-subagent 0.0.6 → 0.0.7
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/README.md +6 -5
- package/dist/index.d.mts +334 -1019
- package/dist/index.mjs +2000 -7392
- package/package.json +38 -30
- package/src/continuation-messages.ts +185 -0
- package/src/delegation-context.ts +39 -0
- package/src/index.ts +627 -0
- package/src/manager.ts +1515 -0
- package/src/rows.ts +44 -0
- package/src/vendor/dsh-subagent/activation.ts +200 -0
- package/src/vendor/dsh-subagent/archive-admission.ts +97 -0
- package/src/vendor/dsh-subagent/assistant-output.ts +77 -0
- package/src/vendor/dsh-subagent/catalog.ts +196 -0
- package/src/vendor/dsh-subagent/child-agent.ts +300 -0
- package/src/vendor/dsh-subagent/continuation-messages.ts +177 -0
- package/src/vendor/dsh-subagent/control-types.ts +146 -0
- package/src/vendor/dsh-subagent/control.ts +123 -0
- package/src/vendor/dsh-subagent/depth.ts +55 -0
- package/src/vendor/dsh-subagent/descriptor.ts +275 -0
- package/src/vendor/dsh-subagent/error.ts +15 -0
- package/src/vendor/dsh-subagent/index.ts +718 -0
- package/src/vendor/dsh-subagent/internal.ts +114 -0
- package/src/vendor/dsh-subagent/lifecycle.ts +172 -0
- package/src/vendor/dsh-subagent/list-children.ts +141 -0
- package/src/vendor/dsh-subagent/manager.ts +1649 -0
- package/src/vendor/dsh-subagent/out-of-process.ts +219 -0
- package/src/vendor/dsh-subagent/projection-types.ts +84 -0
- package/src/vendor/dsh-subagent/projection.ts +205 -0
- package/src/vendor/dsh-subagent/structured.ts +164 -0
- package/src/vendor/dsh-subagent/types.ts +356 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared in-process child composition: the delegation-depth budget, the
|
|
3
|
+
* durable session metadata, the resolved child `AgentOptions`, the delegated
|
|
4
|
+
* policy seed, and the scoped setup used by local activations.
|
|
5
|
+
*
|
|
6
|
+
* @module @deepseek-ai/dsh-subagent/child-agent
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
10
|
+
import type { Agent, AgentOptions, CreateAgentOptions } from "@deepseek-ai/dsh-agent";
|
|
11
|
+
import type { SandboxMode } from "@deepseek-ai/dsh-sandbox";
|
|
12
|
+
import type { Session, SessionId } from "@deepseek-ai/dsh-session";
|
|
13
|
+
import type {} from "@deepseek-ai/dsh-system-prompt";
|
|
14
|
+
import type { ToolRestriction } from "@deepseek-ai/dsh-tools";
|
|
15
|
+
// Type-only: make `ctx.get('sandboxPolicy')`, `ctx.get('approval')`, and
|
|
16
|
+
// `ctx.get('permissionPresets')` resolve to their services when composed — delegation consumes them
|
|
17
|
+
// opportunistically (the documented `ctx.get` pattern), never as a hard dep —
|
|
18
|
+
// and merge the inherited permission session-event payloads.
|
|
19
|
+
import type {} from "@deepseek-ai/dsh-sandbox-policy";
|
|
20
|
+
import type {} from "@deepseek-ai/dsh-user-approval";
|
|
21
|
+
import type {} from "@deepseek-ai/dsh-permission-presets";
|
|
22
|
+
// Type-only: make `ctx.get('agentPresets')` resolve to the preset roster when
|
|
23
|
+
// composed — a child inherits its parent's composition opportunistically (the
|
|
24
|
+
// documented `ctx.get` pattern), never as a hard dep. A rosterless deployment
|
|
25
|
+
// keeps its model-facing rows on the host plane, where the child already sees
|
|
26
|
+
// them through the tool registry's global layer.
|
|
27
|
+
import type {} from "@deepseek-ai/dsh-agent-preset-registry";
|
|
28
|
+
import { delegationDepthOf } from "./depth.ts";
|
|
29
|
+
import { SubagentError } from "./error.ts";
|
|
30
|
+
|
|
31
|
+
/** Thrown when starting a child would exceed the requested depth cap. */
|
|
32
|
+
export class SubagentDepthError extends Error {
|
|
33
|
+
constructor(
|
|
34
|
+
public readonly attemptedDepth: number,
|
|
35
|
+
public readonly maxDepth: number,
|
|
36
|
+
) {
|
|
37
|
+
super(`subagent depth ${attemptedDepth} exceeds maxDepth ${maxDepth}`);
|
|
38
|
+
this.name = "SubagentDepthError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the child's delegation depth from its parent and enforce an optional
|
|
44
|
+
* cap. The persisted parent header is the monotone floor, so a resumed parent
|
|
45
|
+
* cannot delegate as if it were top-level.
|
|
46
|
+
* @param parent - the delegating parent agent.
|
|
47
|
+
* @param maxDepth - optional absolute cap the resolved depth must not exceed.
|
|
48
|
+
* @returns the child's non-negative safe-integer depth.
|
|
49
|
+
* @throws {SubagentDepthError} when the resolved depth exceeds `maxDepth`.
|
|
50
|
+
* @throws {RangeError} when the resolved depth leaves the safe-integer range.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveChildDepth(parent: Agent, maxDepth: number | undefined): number {
|
|
53
|
+
const childDepth = delegationDepthOf(parent) + 1;
|
|
54
|
+
if (!Number.isSafeInteger(childDepth)) {
|
|
55
|
+
throw new RangeError("subagent child depth exceeds the safe-integer range");
|
|
56
|
+
}
|
|
57
|
+
if (maxDepth !== undefined && childDepth > maxDepth) {
|
|
58
|
+
throw new SubagentDepthError(childDepth, maxDepth);
|
|
59
|
+
}
|
|
60
|
+
return childDepth;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the parent values inherited by a child. The latest request header
|
|
65
|
+
* owns provider, model, and reasoning effort after request-time selection;
|
|
66
|
+
* creation options remain the fallback before the first request and retain
|
|
67
|
+
* the configured output-token limit.
|
|
68
|
+
* @param parent - delegating parent Agent.
|
|
69
|
+
* @returns detached Agent options for child-option merging.
|
|
70
|
+
*/
|
|
71
|
+
export function parentAgentOptionsForDelegation(parent: Agent): AgentOptions {
|
|
72
|
+
const requestConfig = parent.session.requestHeader()?.config;
|
|
73
|
+
if (requestConfig === undefined) return { ...parent.options };
|
|
74
|
+
const {
|
|
75
|
+
provider: _createdProvider,
|
|
76
|
+
model: _createdModel,
|
|
77
|
+
reasoningEffort: _createdReasoningEffort,
|
|
78
|
+
...createdOptions
|
|
79
|
+
} = parent.options;
|
|
80
|
+
return {
|
|
81
|
+
...createdOptions,
|
|
82
|
+
provider: requestConfig.provider,
|
|
83
|
+
model: requestConfig.model,
|
|
84
|
+
...(requestConfig.reasoningEffort === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { reasoningEffort: requestConfig.reasoningEffort }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the child's `AgentOptions`: the parent's provider/model,
|
|
92
|
+
* reasoning-effort, and maxTokens values unless the request overrides them,
|
|
93
|
+
* stamped with the child's own delegation depth. Changing the route without
|
|
94
|
+
* naming an effort clears the parent's route-owned effort so the selected
|
|
95
|
+
* model resolves its own default.
|
|
96
|
+
* @param parent - the delegating parent whose route the child inherits.
|
|
97
|
+
* @param requested - per-child overrides, if any.
|
|
98
|
+
* @param childDepth - the resolved delegation depth to stamp.
|
|
99
|
+
* @returns the resolved options for `ctx.agents.create()`.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveChildAgentOptions(
|
|
102
|
+
parent: Agent,
|
|
103
|
+
requested: AgentOptions | undefined,
|
|
104
|
+
childDepth: number,
|
|
105
|
+
): AgentOptions {
|
|
106
|
+
const parentOptions = parentAgentOptionsForDelegation(parent);
|
|
107
|
+
const parentProvider = parentOptions.provider;
|
|
108
|
+
const parentModel = parentOptions.model;
|
|
109
|
+
const parentReasoningEffort = parentOptions.reasoningEffort;
|
|
110
|
+
const parentMaxTokens = parentOptions.maxTokens;
|
|
111
|
+
const resolved: AgentOptions = {
|
|
112
|
+
...(parentProvider !== undefined ? { provider: parentProvider } : {}),
|
|
113
|
+
...(parentModel !== undefined ? { model: parentModel } : {}),
|
|
114
|
+
...(parentReasoningEffort !== undefined ? { reasoningEffort: parentReasoningEffort } : {}),
|
|
115
|
+
...(parentMaxTokens !== undefined ? { maxTokens: parentMaxTokens } : {}),
|
|
116
|
+
...requested,
|
|
117
|
+
subagentDepth: childDepth,
|
|
118
|
+
};
|
|
119
|
+
const routeChanged = resolved.provider !== parentProvider || resolved.model !== parentModel;
|
|
120
|
+
if (routeChanged && requested?.reasoningEffort === undefined) delete resolved.reasoningEffort;
|
|
121
|
+
return resolved;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build the child session's durable creation metadata: the parent's workspace,
|
|
126
|
+
* its direct lineage, coarse product origin, the recursion budget that must
|
|
127
|
+
* survive persistence, the seed boundary that separates inherited parent
|
|
128
|
+
* history from child work, and the composition the child runs under.
|
|
129
|
+
*
|
|
130
|
+
* Omitted selection uses the parent's live composition, including any switch
|
|
131
|
+
* made while the parent was blank.
|
|
132
|
+
* @param parent - the delegating parent agent.
|
|
133
|
+
* @param childDepth - the resolved delegation depth to persist.
|
|
134
|
+
* @param isSeeded - whether this child inherits a parent-log prefix, including an explicitly empty one.
|
|
135
|
+
* @param selectedPreset - independent selection, or omission to inherit the parent.
|
|
136
|
+
* @returns the `meta` for `ctx.agents.create()`.
|
|
137
|
+
*/
|
|
138
|
+
export function childSessionMeta(
|
|
139
|
+
parent: Agent,
|
|
140
|
+
childDepth: number,
|
|
141
|
+
isSeeded: boolean,
|
|
142
|
+
selectedPreset?: string,
|
|
143
|
+
): NonNullable<CreateAgentOptions["meta"]> {
|
|
144
|
+
const parentHeader = parent.session.header;
|
|
145
|
+
const agentPreset = selectedPreset ?? parent.ctx.get("agentPresets")?.composedPreset(parent.ctx);
|
|
146
|
+
return {
|
|
147
|
+
...(parentHeader.cwd !== undefined ? { cwd: parentHeader.cwd } : {}),
|
|
148
|
+
...(agentPreset === undefined ? {} : { agentPreset }),
|
|
149
|
+
parentSession: parentHeader.id,
|
|
150
|
+
isSeeded,
|
|
151
|
+
// Navigation classification only; the descriptor remains the authority
|
|
152
|
+
// for mode and continuation capability.
|
|
153
|
+
origin: "subagent",
|
|
154
|
+
// Durable: the recursion budget must survive persistence and resume.
|
|
155
|
+
delegationDepth: childDepth,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The scoped composition a child agent's creation window applies. */
|
|
160
|
+
export interface ChildComposition {
|
|
161
|
+
/** Independently mounted preset; omission shares the parent's standing generation. */
|
|
162
|
+
readonly agentPreset?: string | undefined;
|
|
163
|
+
/** Historical descriptor persona reapplied only during cold resume. */
|
|
164
|
+
readonly persona?: string | undefined;
|
|
165
|
+
/** Historical descriptor tool restriction reapplied only during cold resume. */
|
|
166
|
+
readonly toolFilter?: ToolRestriction | undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Model-facing delegation-scope statement for every in-process child. A
|
|
171
|
+
* runtime-context contribution rather than a system-prompt section, so the
|
|
172
|
+
* deployment's system prompt stays uniform across parents and children.
|
|
173
|
+
*/
|
|
174
|
+
export const SUBAGENT_DELEGATION_CONTEXT =
|
|
175
|
+
"You are a delegated subagent: your permission scope was fixed when you were started and cannot be " +
|
|
176
|
+
"widened from inside this session — operations that require approval are rejected automatically. " +
|
|
177
|
+
"When the task needs access beyond that scope, do not retry the denied operation; state the " +
|
|
178
|
+
"limitation in your reply so the delegating agent can handle it.";
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Compose a child before publication and restore historical descriptor overrides.
|
|
182
|
+
* Explicit selection mounts the named preset; omission synchronously joins the
|
|
183
|
+
* parent's current generation. Registrations belong to the child's scope.
|
|
184
|
+
* @param childCtx - the child agent's scoped creation context.
|
|
185
|
+
* @param parent - the delegating parent used when no preset is selected.
|
|
186
|
+
* @param composition - independent preset and any overrides read from a historical descriptor.
|
|
187
|
+
* @returns A promise when mounting an independent preset; otherwise completes synchronously.
|
|
188
|
+
*/
|
|
189
|
+
export function applyChildComposition(
|
|
190
|
+
childCtx: Context,
|
|
191
|
+
parent: Agent,
|
|
192
|
+
composition: ChildComposition,
|
|
193
|
+
): void | Promise<void> {
|
|
194
|
+
if (composition.agentPreset !== undefined) {
|
|
195
|
+
const presets = childCtx.get("agentPresets");
|
|
196
|
+
if (presets === undefined) {
|
|
197
|
+
throw new SubagentError(
|
|
198
|
+
"selecting a child agent preset requires the agent-presets service",
|
|
199
|
+
"PRESET_UNAVAILABLE",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return presets.mount(childCtx, composition.agentPreset).then(() => {
|
|
203
|
+
applyChildOverrides(childCtx, composition);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
childCtx.get("agentPresets")?.composeFrom(childCtx, parent.ctx);
|
|
207
|
+
applyChildOverrides(childCtx, composition);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applyChildOverrides(childCtx: Context, composition: ChildComposition): void {
|
|
211
|
+
childCtx.systemPrompt.context({
|
|
212
|
+
name: "subagent:delegation",
|
|
213
|
+
order: childCtx.systemPrompt.getContextOrder("SUBAGENT_DELEGATION"),
|
|
214
|
+
text: SUBAGENT_DELEGATION_CONTEXT,
|
|
215
|
+
});
|
|
216
|
+
if (composition.persona !== undefined) {
|
|
217
|
+
childCtx.systemPrompt.section({
|
|
218
|
+
name: "deployment:persona-prefix",
|
|
219
|
+
order: childCtx.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX"),
|
|
220
|
+
text: composition.persona,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (composition.toolFilter !== undefined) childCtx.tools.restrict(composition.toolFilter);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Policy seeded onto a child session's log at the delegation boundary. */
|
|
227
|
+
export interface DelegatedPolicyOverrides {
|
|
228
|
+
/**
|
|
229
|
+
* The parent's current preset identity when it runs in Auto or Full access;
|
|
230
|
+
* the child keeps it under the pinned `never` approval policy.
|
|
231
|
+
*/
|
|
232
|
+
readonly permissionPreset: "auto" | "danger-full-access" | undefined;
|
|
233
|
+
/** The parent session's explicit sandbox-mode override, or `undefined` without one. */
|
|
234
|
+
readonly sandboxMode: SandboxMode | undefined;
|
|
235
|
+
/**
|
|
236
|
+
* `'never'` whenever the approval capability is composed, `undefined`
|
|
237
|
+
* otherwise: a delegated child acts only within the sandbox scope fixed at
|
|
238
|
+
* delegation, so its asks are rejected deterministically.
|
|
239
|
+
*/
|
|
240
|
+
readonly approvalPolicy: "never" | undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Capture the permission state to seed into one delegation. Call synchronously before
|
|
245
|
+
* the child start's first await: a later parent switch belongs to the
|
|
246
|
+
* parent's future, not to this child. Auto and Full access identities are
|
|
247
|
+
* inherited only through the in-process DSH path so either can replace a stale
|
|
248
|
+
* same-bundle fork value. Only the parent session's explicit sandbox override
|
|
249
|
+
* is captured — never deployment defaults or one-shot grants — and the approval
|
|
250
|
+
* policy is pinned to `'never'` regardless of the parent's own policy.
|
|
251
|
+
* @param parent - the delegating parent agent.
|
|
252
|
+
* @returns the sandbox override (or `undefined` without one) and the approval pin.
|
|
253
|
+
*/
|
|
254
|
+
export function captureDelegatedPolicyOverrides(parent: Agent): DelegatedPolicyOverrides {
|
|
255
|
+
const preset = parent.ctx.get("permissionPresets")?.current(parent.session);
|
|
256
|
+
return {
|
|
257
|
+
permissionPreset: preset === "auto" || preset === "danger-full-access" ? preset : undefined,
|
|
258
|
+
sandboxMode: parent.ctx.get("sandboxPolicy")?.overrideOf(parent.session),
|
|
259
|
+
approvalPolicy: parent.ctx.get("approval") === undefined ? undefined : "never",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Append the captured delegation policy onto the child's own log as
|
|
265
|
+
* `source: 'delegation'` events inside the unpublished creation window, so the
|
|
266
|
+
* child's effective policy is reconstructable from its log alone. Appends land
|
|
267
|
+
* after any fork seed, so fresh policy wins stale seed state; later child
|
|
268
|
+
* switches still win over these events.
|
|
269
|
+
* @param childSession - the unpublished child's session.
|
|
270
|
+
* @param overrides - the policy captured at delegation.
|
|
271
|
+
*/
|
|
272
|
+
export function appendDelegatedPolicyOverrides(
|
|
273
|
+
childSession: Session,
|
|
274
|
+
overrides: DelegatedPolicyOverrides,
|
|
275
|
+
): void {
|
|
276
|
+
if (overrides.sandboxMode !== undefined) {
|
|
277
|
+
childSession.append("sandbox/mode", { mode: overrides.sandboxMode, source: "delegation" });
|
|
278
|
+
}
|
|
279
|
+
if (overrides.approvalPolicy !== undefined) {
|
|
280
|
+
childSession.append("approval/policy", {
|
|
281
|
+
policy: overrides.approvalPolicy,
|
|
282
|
+
source: "delegation",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (overrides.permissionPreset !== undefined) {
|
|
286
|
+
childSession.append("permission/preset", { preset: overrides.permissionPreset });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Identity and lineage inputs shared by every in-process child creation. */
|
|
291
|
+
export interface ChildCreateInputs {
|
|
292
|
+
/** The child's reserved session id. */
|
|
293
|
+
readonly sessionId: SessionId;
|
|
294
|
+
/** The delegating parent agent. */
|
|
295
|
+
readonly parent: Agent;
|
|
296
|
+
/** The resolved delegation depth. */
|
|
297
|
+
readonly childDepth: number;
|
|
298
|
+
/** How many leading seed events came from the parent's log. */
|
|
299
|
+
readonly lineageSeedLength: number;
|
|
300
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-visible messages owned by continuable-subagent orchestration.
|
|
3
|
+
*
|
|
4
|
+
* @module @deepseek-ai/dsh-subagent/continuation-messages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
8
|
+
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
|
+
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
|
|
10
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
11
|
+
import type { SubagentResult } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
/** Durable attribution for one model-authored message between Agents. */
|
|
14
|
+
export interface AgentMessageSource {
|
|
15
|
+
readonly kind: "agent-message";
|
|
16
|
+
/** A message another agent addressed to this one (`relay` context form). */
|
|
17
|
+
readonly form: "relay";
|
|
18
|
+
/** Session id of the Agent whose tool call produced the message. */
|
|
19
|
+
readonly senderSessionId: SessionId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Durable attribution for the runtime's own account of a continuable child
|
|
24
|
+
* settling. Deliberately a different kind from
|
|
25
|
+
* {@link AgentMessageSource}: an Agent message is content the sender chose,
|
|
26
|
+
* while this message is the manager stating what became of the child, and a
|
|
27
|
+
* transcript that merged them would credit the child with words it never wrote.
|
|
28
|
+
*/
|
|
29
|
+
export interface SubagentSettledMessageSource {
|
|
30
|
+
readonly kind: "subagent-settled";
|
|
31
|
+
/** A runtime account shown without expanding the row (`notice` context form). */
|
|
32
|
+
readonly form: "notice";
|
|
33
|
+
/** One-line account of how the child ended. */
|
|
34
|
+
readonly summary: string;
|
|
35
|
+
/** Session id of the child that settled. */
|
|
36
|
+
readonly senderSessionId: SessionId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare module "@deepseek-ai/dsh-llm" {
|
|
40
|
+
interface MessageSourceMap {
|
|
41
|
+
"agent-message": AgentMessageSource;
|
|
42
|
+
"subagent-settled": SubagentSettledMessageSource;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Build durable attribution for one adjacent-Agent message. */
|
|
47
|
+
function agentMessageSource(sender: Agent): AgentMessageSource {
|
|
48
|
+
return {
|
|
49
|
+
kind: "agent-message",
|
|
50
|
+
form: "relay",
|
|
51
|
+
senderSessionId: sender.id,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the model-visible and durable representation of one adjacent-Agent message.
|
|
57
|
+
* @param sender - exact live Agent that authored the message.
|
|
58
|
+
* @param content - model-visible message blocks supplied by the sender.
|
|
59
|
+
* @returns the durable user-message representation delivered to the recipient.
|
|
60
|
+
*/
|
|
61
|
+
export function createAgentMessage(
|
|
62
|
+
sender: Agent,
|
|
63
|
+
content: ContentBlock[],
|
|
64
|
+
): ReturnType<typeof createUserMessage> {
|
|
65
|
+
return createUserMessage({
|
|
66
|
+
content: [{ type: "text" as const, text: `Agent ${sender.id} sent a message: ` }, ...content],
|
|
67
|
+
source: agentMessageSource(sender),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Append adjacent-Agent return guidance to a continuable child's initial task.
|
|
73
|
+
* @param parentId - durable parent session id named in the guidance.
|
|
74
|
+
* @param prompt - initial model-visible task blocks.
|
|
75
|
+
* @returns task blocks followed by the continuable return guidance.
|
|
76
|
+
*/
|
|
77
|
+
export function withContinuableReturnGuidance(
|
|
78
|
+
parentId: SessionId,
|
|
79
|
+
prompt: ContentBlock[],
|
|
80
|
+
): ContentBlock[] {
|
|
81
|
+
const encodedParentId = JSON.stringify(parentId);
|
|
82
|
+
return [
|
|
83
|
+
...prompt,
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text:
|
|
87
|
+
`Your parent agent id is ${encodedParentId}. Before you finish, send your result to that agent with ` +
|
|
88
|
+
`send_message({ agent_id: ${encodedParentId}, message: "<self-contained result>" }). The parent shares ` +
|
|
89
|
+
"your workspace but does not automatically receive your transcript, tool output, or reasoning. Send " +
|
|
90
|
+
"earlier messages as well when a finding changes what the parent should do next; sending a message " +
|
|
91
|
+
"does not end your turn.",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* One line telling a parent that a background child is finished and why, in
|
|
98
|
+
* the parent's own task vocabulary.
|
|
99
|
+
* @param childId - the durable child the parent knows by id.
|
|
100
|
+
* @param stopReason - how the child's last ordinary turn ended.
|
|
101
|
+
* @param continuable - whether the child accepts a later message.
|
|
102
|
+
* @returns the model-facing opening line of the settlement notice.
|
|
103
|
+
*/
|
|
104
|
+
function settlementSummary(
|
|
105
|
+
childId: SessionId,
|
|
106
|
+
stopReason: SubagentResult["stopReason"],
|
|
107
|
+
continuable: boolean,
|
|
108
|
+
): string {
|
|
109
|
+
const subject = `Background subagent ${childId}`;
|
|
110
|
+
switch (stopReason) {
|
|
111
|
+
case "completed":
|
|
112
|
+
return continuable
|
|
113
|
+
? `${subject} finished and will do no further work unless you send it more.`
|
|
114
|
+
: `${subject} finished. It cannot receive follow-up messages.`;
|
|
115
|
+
case "aborted":
|
|
116
|
+
return `${subject} was stopped before it finished.`;
|
|
117
|
+
case "max-tokens":
|
|
118
|
+
return `${subject} ran out of room before it finished.`;
|
|
119
|
+
// A pre-step rejection — a hook deny, a policy plugin — discarded input
|
|
120
|
+
// the child had claimed, so the parent must not treat the task as done.
|
|
121
|
+
case "refusal":
|
|
122
|
+
return `${subject} declined the task.`;
|
|
123
|
+
case "error":
|
|
124
|
+
return `${subject} failed before it finished.`;
|
|
125
|
+
/* v8 ignore next 4 -- `SubagentResult['stopReason']` is merge-extensible, so this arm
|
|
126
|
+
* needs a backend that adds a variant; an unnameable ending is reported as unfinished
|
|
127
|
+
* rather than silently as success. */
|
|
128
|
+
default:
|
|
129
|
+
return `${subject} ended abnormally (${String(stopReason)}) before it finished.`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build the runtime-owned settlement notice from the child's nonempty closing text.
|
|
135
|
+
* @param childId - durable child session id named in the notice.
|
|
136
|
+
* @param terminal - recorded terminal state for the settled Activation.
|
|
137
|
+
* @param continuable - whether the child can accept another task.
|
|
138
|
+
* @returns the durable user-message representation delivered to the parent.
|
|
139
|
+
*/
|
|
140
|
+
export function createSettlementMessage(
|
|
141
|
+
childId: SessionId,
|
|
142
|
+
terminal: SubagentResult,
|
|
143
|
+
continuable = true,
|
|
144
|
+
): ReturnType<typeof createUserMessage> {
|
|
145
|
+
const summary = settlementSummary(childId, terminal.stopReason, continuable);
|
|
146
|
+
// Parent providers receive this notice as a user message and may reject
|
|
147
|
+
// nontext assistant blocks. Keep this conversion local so SDK/UI consumers
|
|
148
|
+
// retain the complete child output.
|
|
149
|
+
const closingText = terminal.output.flatMap((block) =>
|
|
150
|
+
block.type === "text" && block.text.length > 0 ? [block] : [],
|
|
151
|
+
);
|
|
152
|
+
return createUserMessage({
|
|
153
|
+
content: [
|
|
154
|
+
{ type: "text" as const, text: summary },
|
|
155
|
+
...(closingText.length === 0
|
|
156
|
+
? [{ type: "text" as const, text: "It left no closing message." }]
|
|
157
|
+
: [{ type: "text" as const, text: "Its closing message:" }, ...closingText]),
|
|
158
|
+
...(terminal.structured !== undefined
|
|
159
|
+
? [
|
|
160
|
+
{
|
|
161
|
+
type: "text" as const,
|
|
162
|
+
text: `Structured result: ${JSON.stringify(terminal.structured)}`,
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
: []),
|
|
166
|
+
...(terminal.diagnostic !== undefined
|
|
167
|
+
? [{ type: "text" as const, text: terminal.diagnostic }]
|
|
168
|
+
: []),
|
|
169
|
+
],
|
|
170
|
+
source: {
|
|
171
|
+
kind: "subagent-settled" as const,
|
|
172
|
+
form: "notice" as const,
|
|
173
|
+
summary: boundContextSummary(summary),
|
|
174
|
+
senderSessionId: childId,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-safe recursive catalog rows and browser continuation requests,
|
|
3
|
+
* receipts, and failures.
|
|
4
|
+
*
|
|
5
|
+
* @module @deepseek-ai/dsh-subagent/control-types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PromptContentPart } from "@deepseek-ai/dsh-attachment/types";
|
|
9
|
+
import type { Branded } from "@deepseek-ai/dsh-brand";
|
|
10
|
+
import type { MessageId } from "@deepseek-ai/dsh-llm/brand";
|
|
11
|
+
import type { SessionId } from "@deepseek-ai/dsh-session/types";
|
|
12
|
+
// Type-only: the Workspace registry's archive-admission family map this runtime merges `subagent` into.
|
|
13
|
+
import type {} from "@deepseek-ai/dsh-workspace/types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Client-minted identity of one browser prompt, persisted on the exact accepted
|
|
17
|
+
* message. It carries the Session Controller's `session-request-id` brand so a
|
|
18
|
+
* subagent prompt and an ordinary Session prompt share one identity
|
|
19
|
+
* vocabulary; that package depends on this one, so the brand is spelled here
|
|
20
|
+
* rather than imported.
|
|
21
|
+
*/
|
|
22
|
+
export type SubagentPromptRequestId = Branded<"session-request-id">;
|
|
23
|
+
|
|
24
|
+
/** Shared child fields for recursive catalog listing. */
|
|
25
|
+
export type SubagentCatalogRow = {
|
|
26
|
+
/** The durable child session id, stable across Activations. */
|
|
27
|
+
readonly id: SessionId;
|
|
28
|
+
/**
|
|
29
|
+
* Whether recursive catalog listing observed a resident Session. This
|
|
30
|
+
* does not encode a durable outcome or guarantee continuation delivery.
|
|
31
|
+
*/
|
|
32
|
+
readonly activity: "running" | "inactive";
|
|
33
|
+
} & (
|
|
34
|
+
| {
|
|
35
|
+
/** A terminal one-shot child. */
|
|
36
|
+
readonly mode: "one-shot";
|
|
37
|
+
/** Optional durable creation label from the parent catalog. */
|
|
38
|
+
readonly label?: string;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
/** An external execution without a local child Session. */
|
|
42
|
+
readonly mode: "external";
|
|
43
|
+
/** Optional durable creation label from the parent catalog. */
|
|
44
|
+
readonly label?: string;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
/** A resumable conversation. */
|
|
48
|
+
readonly mode: "continuable";
|
|
49
|
+
/** Durable creation label from the parent catalog. */
|
|
50
|
+
readonly label: string;
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One recursive catalog row. Traversal may also return diagnostics for
|
|
56
|
+
* unknown catalog modes or unreadable child catalogs.
|
|
57
|
+
*/
|
|
58
|
+
export type SubagentListEntry =
|
|
59
|
+
| (SubagentCatalogRow & {
|
|
60
|
+
readonly kind: "child";
|
|
61
|
+
/** Whether the child catalog contains a direct child. */
|
|
62
|
+
readonly hasChildren: boolean;
|
|
63
|
+
})
|
|
64
|
+
| {
|
|
65
|
+
readonly kind: "diagnostic";
|
|
66
|
+
/** The candidate's session id. */
|
|
67
|
+
readonly id: SessionId;
|
|
68
|
+
/**
|
|
69
|
+
* `corrupt` means the child catalog read found invalid or conflicting
|
|
70
|
+
* Session data; `unavailable` means it could not be read. Either failure
|
|
71
|
+
* stops that branch. `unsupported` means the parent catalog records an
|
|
72
|
+
* unknown child mode; its readable catalog is still traversed.
|
|
73
|
+
*/
|
|
74
|
+
readonly reason: "corrupt" | "unsupported" | "unavailable";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Durable parent/child browsing address; unknown mode is resolved when child history is read. */
|
|
78
|
+
export type SubagentAddress = {
|
|
79
|
+
readonly parentSessionId: SessionId;
|
|
80
|
+
readonly childSessionId: SessionId;
|
|
81
|
+
} & (
|
|
82
|
+
| { readonly mode: "one-shot" }
|
|
83
|
+
| { readonly mode: "continuable" }
|
|
84
|
+
| { readonly mode: "unknown" }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/** One Agent inbox destination, as the prompt request selects it. */
|
|
88
|
+
export type SubagentDelivery = SubagentPromptRequest["delivery"];
|
|
89
|
+
|
|
90
|
+
/** One human message addressed to a continuable direct child. */
|
|
91
|
+
export interface SubagentPromptRequest {
|
|
92
|
+
/** Identity persisted on the accepted message, minted before the call. */
|
|
93
|
+
readonly requestId: SubagentPromptRequestId;
|
|
94
|
+
readonly parentSessionId: SessionId;
|
|
95
|
+
readonly childSessionId: SessionId;
|
|
96
|
+
/** Required discriminator retained from the browser control address. */
|
|
97
|
+
readonly mode: "continuable";
|
|
98
|
+
/** Whether this message queues a later turn or targets the nearest step. */
|
|
99
|
+
readonly delivery: "queue" | "steer";
|
|
100
|
+
/**
|
|
101
|
+
* Browser prompt parts delivered as the child's user message. The Host
|
|
102
|
+
* admits and persists image parts before delivery, so the wire never
|
|
103
|
+
* carries a durable attachment reference the caller could fabricate.
|
|
104
|
+
*/
|
|
105
|
+
readonly content: readonly PromptContentPart[];
|
|
106
|
+
/** Optional browser zone sampled for this exact human prompt. */
|
|
107
|
+
readonly clientTimeZone?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Inbox identity returned once the continuation accepts one human message. */
|
|
111
|
+
export interface SubagentPromptReceipt {
|
|
112
|
+
readonly messageId: MessageId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Uniform acknowledgement that one interrupt request was admitted. */
|
|
116
|
+
export interface SubagentInterruptReceipt {
|
|
117
|
+
readonly accepted: true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Failure details the control surface answers with. Prompts and interrupts
|
|
122
|
+
* share these failures with the Client Remote result.
|
|
123
|
+
*/
|
|
124
|
+
declare module "@deepseek-ai/dsh-workspace/types" {
|
|
125
|
+
interface SessionActivityKindMap {
|
|
126
|
+
/** A subagent session delegated from this session (at any depth) is inside a turn. */
|
|
127
|
+
subagent: true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
declare module "@deepseek-ai/dsh-typert-protocol" {
|
|
132
|
+
interface RemoteErrorDetailsMap {
|
|
133
|
+
/** A browser-supplied zone is neither UTC nor a canonical IANA name. */
|
|
134
|
+
"subagent/invalid-time-zone": { readonly value: string };
|
|
135
|
+
/** No live Agent carries the addressed parent session. */
|
|
136
|
+
"subagent/parent-unavailable": { readonly parentSessionId: SessionId };
|
|
137
|
+
/** The addressed child cannot take a continuation. */
|
|
138
|
+
"subagent/not-resumable": { readonly childSessionId: SessionId };
|
|
139
|
+
/** The claimed parent does not own the addressed child. */
|
|
140
|
+
"subagent/unauthorized": { readonly childSessionId: SessionId };
|
|
141
|
+
/** Image admission or model image-capability refusal. */
|
|
142
|
+
"subagent/attachment-invalid": { readonly reason: string };
|
|
143
|
+
/** The child exists but its inbox cannot admit the message now. */
|
|
144
|
+
"subagent/delivery-unavailable": { readonly childSessionId: SessionId };
|
|
145
|
+
}
|
|
146
|
+
}
|