@morlay/dsh-subagent 0.0.5 → 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
package/src/rows.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 本包作为能力包发布的**装配数据**:行本身在部署那一层装(`packages/bundles/session-mode-profile`
|
|
2
|
+
// 引用它渲染 patch)。
|
|
3
|
+
//
|
|
4
|
+
// 接管方式是**按官方行 id 复用**:Loader 对同 id 的条目复用同一个 Entry、后者替换入口 options,所以行
|
|
5
|
+
// id 与 settings namespace 都还是 `subagent`(官方设置卡照常可用),换掉的只是实现包。顺序前提:本包排在
|
|
6
|
+
// `@deepseek-ai/dsh-base` 之后;模型白名单服务行仍留官方卡片第二段。
|
|
7
|
+
|
|
8
|
+
// 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。
|
|
9
|
+
export interface PatchRow {
|
|
10
|
+
readonly id?: string;
|
|
11
|
+
readonly disabled?: boolean;
|
|
12
|
+
readonly insert?: readonly RowEntry[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// `insert` 里的一个条目。
|
|
16
|
+
export interface RowEntry {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly config?: Readonly<Record<string, unknown>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SubagentRowsOptions {
|
|
23
|
+
// 把中文回报指引**限制**到这些 preset(本行 `config.localizedReturnGuidancePresets`)。
|
|
24
|
+
// 不传就是不限:任意 preset(含官方四个 shipped)与还没绑 preset 的会话都用我们的中文文案。
|
|
25
|
+
readonly localizedReturnGuidancePresets?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 服务接管的装配行:占官方 `subagent` 那个行 id。
|
|
29
|
+
export function subagentRows(options: SubagentRowsOptions = {}): readonly PatchRow[] {
|
|
30
|
+
const presets = options.localizedReturnGuidancePresets;
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
insert: [
|
|
34
|
+
{
|
|
35
|
+
id: "subagent",
|
|
36
|
+
name: "@morlay/dsh-subagent",
|
|
37
|
+
...(presets === undefined
|
|
38
|
+
? {}
|
|
39
|
+
: { config: { localizedReturnGuidancePresets: [...presets] } }),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** Execution-period records, capacity slots, child serialization, and local output capture.
|
|
2
|
+
* @module @deepseek-ai/dsh-subagent/activation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Agent, AgentHandle } from "@deepseek-ai/dsh-agent";
|
|
6
|
+
import type { SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
7
|
+
import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
8
|
+
import { finalAssistantOutput } from "./assistant-output.ts";
|
|
9
|
+
import { epochStopReason } from "./lifecycle.ts";
|
|
10
|
+
import type { ActivationObserver } from "./lifecycle.ts";
|
|
11
|
+
import { SubagentError } from "./error.ts";
|
|
12
|
+
import type { StructuredAttachment } from "./structured.ts";
|
|
13
|
+
import type { SubagentResult, SubagentRun } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
/** Process-local slots shared through uninterrupted continuable parent links. */
|
|
16
|
+
export class ActivationPool {
|
|
17
|
+
private readonly slots = new Set<symbol>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reserve before reconstruction; release also tolerates unpublished rollback.
|
|
21
|
+
* @param capacity - the maximum simultaneous activations in this pool.
|
|
22
|
+
* @returns an idempotent slot release.
|
|
23
|
+
*/
|
|
24
|
+
reserve(capacity: number): () => void {
|
|
25
|
+
if (this.slots.size >= capacity) {
|
|
26
|
+
throw new SubagentError(
|
|
27
|
+
`subagent limit reached (active child limit: ${capacity}); wait for an existing child to finish ` +
|
|
28
|
+
"or complete this work with the current agents",
|
|
29
|
+
"ACTIVATION_LIMIT_REACHED",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const slot = Symbol();
|
|
33
|
+
this.slots.add(slot);
|
|
34
|
+
return () => {
|
|
35
|
+
this.slots.delete(slot);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One residency epoch for a local child or an external execution.
|
|
42
|
+
* The manager owns admission, execution handles, and resource release.
|
|
43
|
+
*/
|
|
44
|
+
interface ActivationBase {
|
|
45
|
+
/** Shared capacity for this Activation and all its managed descendants. */
|
|
46
|
+
readonly pool: ActivationPool;
|
|
47
|
+
/** Return this epoch's slot after its handle has finished disposal. */
|
|
48
|
+
readonly releaseSlot: () => void;
|
|
49
|
+
/** The durable child this Activation is an epoch of. */
|
|
50
|
+
readonly childId: SessionId;
|
|
51
|
+
readonly parent: Agent;
|
|
52
|
+
readonly delivery: "parent" | "caller";
|
|
53
|
+
readonly result: PromiseWithResolvers<SubagentResult>;
|
|
54
|
+
/** Published synchronously before teardown; subsequent callers share this transaction. */
|
|
55
|
+
closing: Promise<void> | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Exact live Agent ancestry observed when this Activation materialized.
|
|
58
|
+
* Weak membership preserves host-scope identity across an intermediate
|
|
59
|
+
* ancestor leaving the registry without retaining that ancestor's runtime.
|
|
60
|
+
*/
|
|
61
|
+
readonly ancestry: WeakSet<Agent>;
|
|
62
|
+
/**
|
|
63
|
+
* Session ids of the child Activations this one owns. Because one Session has
|
|
64
|
+
* at most one live Activation, the id identifies the live child without
|
|
65
|
+
* another runtime-incarnation reference. Non-empty blocks settlement.
|
|
66
|
+
*/
|
|
67
|
+
readonly ownedChildren: Set<SessionId>;
|
|
68
|
+
/** The lifecycle observer that emits this epoch's start and terminal edges. */
|
|
69
|
+
readonly observer: ActivationObserver;
|
|
70
|
+
/**
|
|
71
|
+
* Whether any delivery to this child was ever accepted. A materialization
|
|
72
|
+
* rolled back before its first acceptance is a child the caller was told does
|
|
73
|
+
* not exist, so its teardown owes the parent no settlement account.
|
|
74
|
+
*/
|
|
75
|
+
announced: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Resources retained only for the selected execution kind. */
|
|
79
|
+
export type ActivationResource =
|
|
80
|
+
| {
|
|
81
|
+
readonly kind: "local";
|
|
82
|
+
readonly handle: AgentHandle;
|
|
83
|
+
readonly outputStart: SessionLogOffset;
|
|
84
|
+
readonly structured: StructuredAttachment | undefined;
|
|
85
|
+
/** Latest live failure not superseded by a later committed turn ending. */
|
|
86
|
+
failureAt: SessionLogOffset | undefined;
|
|
87
|
+
/** Renewed when local settlement must recheck input or owned children. */
|
|
88
|
+
poke: PromiseWithResolvers<void>;
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
readonly kind: "external";
|
|
92
|
+
readonly run: SubagentRun;
|
|
93
|
+
readonly controller: AbortController;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** One managed execution period with its provider-specific resources. */
|
|
97
|
+
export type Activation = ActivationBase & ActivationResource;
|
|
98
|
+
|
|
99
|
+
/** Local residency with input admission and Session-backed result capture. */
|
|
100
|
+
export type LocalActivation = Extract<Activation, { kind: "local" }>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read the live Agent retained by local execution.
|
|
104
|
+
* @param activation - the managed local or external execution.
|
|
105
|
+
* @returns the local Agent, absent for an external run.
|
|
106
|
+
*/
|
|
107
|
+
export function activationAgent(activation: Activation): Agent | undefined {
|
|
108
|
+
return activation.kind === "local" ? activation.handle.agent : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Require local residency before accepting further input.
|
|
113
|
+
* @param activation - execution targeted by message delivery.
|
|
114
|
+
* @returns the local activation.
|
|
115
|
+
* @throws when the external backend cannot accept another message.
|
|
116
|
+
*/
|
|
117
|
+
export function requireLocalActivation(activation: Activation): LocalActivation {
|
|
118
|
+
switch (activation.kind) {
|
|
119
|
+
case "local":
|
|
120
|
+
return activation;
|
|
121
|
+
case "external":
|
|
122
|
+
throw new SubagentError(
|
|
123
|
+
`subagent "${activation.childId}" does not accept follow-up input`,
|
|
124
|
+
"NOT_CONTINUABLE",
|
|
125
|
+
);
|
|
126
|
+
/* v8 ignore next 2 -- Both execution kinds are handled above. */
|
|
127
|
+
default:
|
|
128
|
+
return assertNever(activation);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Retain local failures whose turn ending could not be committed.
|
|
134
|
+
* @param activation - exact local Agent and epoch receiving the scoped observers.
|
|
135
|
+
* @param wake - notify its settlement watcher when failure changes readiness.
|
|
136
|
+
*/
|
|
137
|
+
export function observeLocalFailure(activation: LocalActivation, wake: () => void): void {
|
|
138
|
+
const child = activation.handle.agent;
|
|
139
|
+
child.ctx.on("agent/error", ({ agent }) => {
|
|
140
|
+
if (agent !== child) return;
|
|
141
|
+
activation.failureAt = child.session.seq;
|
|
142
|
+
wake();
|
|
143
|
+
});
|
|
144
|
+
child.ctx.on("session/event", (session, event) => {
|
|
145
|
+
if (
|
|
146
|
+
session === child.session &&
|
|
147
|
+
event.type === "turn/end" &&
|
|
148
|
+
activation.failureAt !== undefined &&
|
|
149
|
+
event.seq >= activation.failureAt
|
|
150
|
+
) {
|
|
151
|
+
activation.failureAt = undefined;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Capture local output and retain failures without a later committed turn ending.
|
|
158
|
+
* @param activation - the settled local execution and any structured result.
|
|
159
|
+
* @returns final output, stop reason, and committed structured value.
|
|
160
|
+
*/
|
|
161
|
+
export function captureLocalResult(activation: LocalActivation): SubagentResult {
|
|
162
|
+
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
|
|
163
|
+
const events = activation.handle.agent.session.snapshotEvents(activation.outputStart);
|
|
164
|
+
const output = finalAssistantOutput(events) ?? [];
|
|
165
|
+
const stopReason = activation.failureAt === undefined ? epochStopReason(events) : "error";
|
|
166
|
+
const structured = activation.structured;
|
|
167
|
+
if (structured !== undefined) {
|
|
168
|
+
const captured = structured.captured();
|
|
169
|
+
if (captured !== undefined) return { output, stopReason, structured: captured.value };
|
|
170
|
+
if (stopReason === "completed") return { output, stopReason: "error" };
|
|
171
|
+
}
|
|
172
|
+
return { output, stopReason };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Serialize each durable child's delivery, release, and disposal. */
|
|
176
|
+
export class ChildLock {
|
|
177
|
+
private tails = new Map<SessionId, Promise<unknown>>();
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run `operation` after every previously queued operation for `childId`.
|
|
181
|
+
* @param childId - the durable child whose operations are linearized.
|
|
182
|
+
* @param operation - the critical section to run in order.
|
|
183
|
+
* @returns the operation's own settlement.
|
|
184
|
+
*/
|
|
185
|
+
run<T>(childId: SessionId, operation: () => Promise<T>): Promise<T> {
|
|
186
|
+
const previous = this.tails.get(childId) ?? Promise.resolve();
|
|
187
|
+
const result = previous.then(operation, operation);
|
|
188
|
+
// Absorb rejections in the chaining tail so one failed critical section
|
|
189
|
+
// cannot reject an unrelated later caller.
|
|
190
|
+
const tail = result.then(
|
|
191
|
+
() => undefined,
|
|
192
|
+
() => undefined,
|
|
193
|
+
);
|
|
194
|
+
this.tails.set(childId, tail);
|
|
195
|
+
void tail.then(() => {
|
|
196
|
+
if (this.tails.get(childId) === tail) this.tails.delete(childId);
|
|
197
|
+
});
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `subagent` family of the Workspace registry's archive admission: which
|
|
3
|
+
* subagent descendants of a Session are still inside a turn, and how they
|
|
4
|
+
* stop when the Session is archived with its work.
|
|
5
|
+
*
|
|
6
|
+
* @module @deepseek-ai/dsh-subagent
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
10
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
11
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
12
|
+
import type {} from "@deepseek-ai/dsh-session-query";
|
|
13
|
+
import type { SessionActivity, SessionActivityItem } from "@deepseek-ai/dsh-workspace";
|
|
14
|
+
import { foldSubagentDescriptor } from "./descriptor.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Answer `workspace/session-activity` with the running subagent descendants of
|
|
18
|
+
* the asked Session, and `workspace/session-stop` by cancelling each of them
|
|
19
|
+
* as their parent would. Both listeners live as long as `ctx`'s fiber.
|
|
20
|
+
* @param ctx - context carrying the Agent registry; the Session query service is optional and only supplies labels.
|
|
21
|
+
*/
|
|
22
|
+
export function installSubagentArchiveAdmission(ctx: Context): void {
|
|
23
|
+
ctx.on("workspace/session-activity", async ({ sessionId }, next) => {
|
|
24
|
+
const running = runningDescendants(ctx, sessionId);
|
|
25
|
+
const rest = await next();
|
|
26
|
+
if (running.length === 0) return rest;
|
|
27
|
+
const own: SessionActivity = {
|
|
28
|
+
kind: "subagent",
|
|
29
|
+
items: await Promise.all(running.map((child) => describe(ctx, child))),
|
|
30
|
+
};
|
|
31
|
+
return [own, ...rest];
|
|
32
|
+
});
|
|
33
|
+
ctx.on("workspace/session-stop", ({ sessionId }) => {
|
|
34
|
+
for (const child of runningDescendants(ctx, sessionId)) {
|
|
35
|
+
try {
|
|
36
|
+
child.cancel({ kind: "parent" });
|
|
37
|
+
} catch (error: unknown) {
|
|
38
|
+
// One child refusing its cancel must not keep its siblings running for an archived parent.
|
|
39
|
+
ctx.logger.warn(
|
|
40
|
+
`subagent: cancelling "${child.id}" for an archived Session failed: ${String(error)}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Live subagent descendants inside a turn, by durable lineage: a child whose
|
|
49
|
+
* header names its parent and carries the subagent origin this package
|
|
50
|
+
* records, at any depth. A fork shares the lineage field without the origin
|
|
51
|
+
* and is an independent conversation, so it never holds its source. Lineage
|
|
52
|
+
* is read as data, so a damaged header chain that loops is visited once.
|
|
53
|
+
*/
|
|
54
|
+
function runningDescendants(ctx: Context, rootId: SessionId): Agent[] {
|
|
55
|
+
const childrenOf = new Map<SessionId, Agent[]>();
|
|
56
|
+
for (const agent of ctx.agents.list()) {
|
|
57
|
+
const { parentSession, origin } = agent.session.header;
|
|
58
|
+
if (parentSession === undefined || origin !== "subagent") continue;
|
|
59
|
+
const siblings = childrenOf.get(parentSession) ?? [];
|
|
60
|
+
siblings.push(agent);
|
|
61
|
+
childrenOf.set(parentSession, siblings);
|
|
62
|
+
}
|
|
63
|
+
const running: Agent[] = [];
|
|
64
|
+
const pending = [rootId];
|
|
65
|
+
const visited = new Set<SessionId>();
|
|
66
|
+
while (pending.length > 0) {
|
|
67
|
+
const parentId = pending.shift() as SessionId;
|
|
68
|
+
if (visited.has(parentId)) continue;
|
|
69
|
+
visited.add(parentId);
|
|
70
|
+
for (const child of childrenOf.get(parentId) ?? []) {
|
|
71
|
+
if (child.status === "running") running.push(child);
|
|
72
|
+
pending.push(child.id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return running;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The child's activity item: its id, plus the durable creation label its
|
|
80
|
+
* descriptor carries, read through a live Session observation. Without the
|
|
81
|
+
* Session query service, or with a descriptor that is absent or unreadable,
|
|
82
|
+
* the item names the child by id alone.
|
|
83
|
+
*/
|
|
84
|
+
async function describe(ctx: Context, child: Agent): Promise<SessionActivityItem> {
|
|
85
|
+
const query = ctx.get("sessionQuery");
|
|
86
|
+
if (query === undefined) return { id: child.id };
|
|
87
|
+
try {
|
|
88
|
+
using observation = await query.observeSession(child.id, { projectionMode: "none" });
|
|
89
|
+
const label = foldSubagentDescriptor(
|
|
90
|
+
observation.events.slice(observation.inheritedEventCount),
|
|
91
|
+
)?.label;
|
|
92
|
+
return label === undefined ? { id: child.id } : { id: child.id, label };
|
|
93
|
+
} catch {
|
|
94
|
+
// A damaged descriptor is data damage in the child; the report still names the child.
|
|
95
|
+
return { id: child.id };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical selection of a child's final assistant output. Backend run results
|
|
3
|
+
* and `subagent/end.lastAssistantMessage` apply the same rule: select the last
|
|
4
|
+
* non-empty assistant message. An empty-content message records usage only
|
|
5
|
+
* when the loop appends it after a max-tokens step with no executable blocks,
|
|
6
|
+
* so it does not replace earlier output. If no non-empty message exists,
|
|
7
|
+
* select the accumulated assistant text. Selection is independent of the
|
|
8
|
+
* run's stop reason.
|
|
9
|
+
*
|
|
10
|
+
* @module @deepseek-ai/dsh-subagent/assistant-output
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { joinAssistantStreamText, type ContentBlock } from "@deepseek-ai/dsh-llm";
|
|
14
|
+
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Incremental fold of the selection rule, for backends that observe a child's
|
|
18
|
+
* output as it streams: session-event backends {@link push} each event, and
|
|
19
|
+
* transports without session events (ACP content chunks) {@link pushText} raw
|
|
20
|
+
* text into the same streamed fallback.
|
|
21
|
+
*/
|
|
22
|
+
export class AssistantOutputFold {
|
|
23
|
+
private message: readonly ContentBlock[] | undefined;
|
|
24
|
+
private partial: string[] = [];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fold one session event: a non-empty assistant message becomes the
|
|
28
|
+
* candidate final answer, while its embedded stream and any log-only attempt
|
|
29
|
+
* extend the streamed fallback; every other event contributes nothing.
|
|
30
|
+
* @param event - the next observed session event.
|
|
31
|
+
*/
|
|
32
|
+
push(event: SessionEvent): void {
|
|
33
|
+
if (event.type === "assistant/message") {
|
|
34
|
+
const content = event.data.message.content;
|
|
35
|
+
if (content.length > 0) this.message = content;
|
|
36
|
+
}
|
|
37
|
+
if (event.type === "assistant/message" || event.type === "assistant/attempt") {
|
|
38
|
+
this.pushText(joinAssistantStreamText(event.data.stream));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extend the streamed fallback with text observed outside session events.
|
|
44
|
+
* @param text - the next streamed text piece (an empty piece is a no-op).
|
|
45
|
+
*/
|
|
46
|
+
pushText(text: string): void {
|
|
47
|
+
if (text.length > 0) this.partial.push(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Select the final output folded so far.
|
|
52
|
+
* @returns the last non-empty assistant message, else the accumulated
|
|
53
|
+
* streamed text, or `undefined` when the child produced neither.
|
|
54
|
+
*/
|
|
55
|
+
collect(): readonly ContentBlock[] | undefined {
|
|
56
|
+
if (this.message !== undefined) return this.message;
|
|
57
|
+
const text = this.partial.join("");
|
|
58
|
+
return text.length > 0 ? [{ type: "text", text }] : undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply the selection rule to one complete child-owned event suffix.
|
|
64
|
+
* @param events - the child-owned events (after any seed or epoch boundary).
|
|
65
|
+
* @returns the selected output, or `undefined` when the child produced none.
|
|
66
|
+
*/
|
|
67
|
+
export function finalAssistantOutput(
|
|
68
|
+
events: readonly SessionEvent[],
|
|
69
|
+
): readonly ContentBlock[] | undefined {
|
|
70
|
+
// TODO: this folds the complete suffix once per run/epoch settlement. If a
|
|
71
|
+
// long continuable epoch ever profiles hot here, scan backward with early
|
|
72
|
+
// exit for the last non-empty message and fold text deltas only on the
|
|
73
|
+
// no-message fallback.
|
|
74
|
+
const fold = new AssistantOutputFold();
|
|
75
|
+
for (const event of events) fold.push(event);
|
|
76
|
+
return fold.collect();
|
|
77
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-owned durable subagent catalog events and their chunked projection.
|
|
3
|
+
*
|
|
4
|
+
* @module @deepseek-ai/dsh-subagent/catalog
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import {
|
|
9
|
+
appendChunkedList,
|
|
10
|
+
chunkedListSchema,
|
|
11
|
+
iterateChunkedList,
|
|
12
|
+
} from "@deepseek-ai/dsh-chunked-list";
|
|
13
|
+
import type { ChunkedList } from "@deepseek-ai/dsh-chunked-list";
|
|
14
|
+
import type {
|
|
15
|
+
Session,
|
|
16
|
+
SessionEvent,
|
|
17
|
+
SessionHeader,
|
|
18
|
+
SessionId,
|
|
19
|
+
SessionLogOffset,
|
|
20
|
+
} from "@deepseek-ai/dsh-session";
|
|
21
|
+
import type { ProjectionDefinition } from "@deepseek-ai/dsh-session-projection";
|
|
22
|
+
import type { SubagentCatalogEntry } from "./projection-types.ts";
|
|
23
|
+
|
|
24
|
+
/** Catalog payload version emitted by local child creation. */
|
|
25
|
+
export const SUBAGENT_CATALOG_VERSION = 0;
|
|
26
|
+
|
|
27
|
+
type KnownCatalogMode =
|
|
28
|
+
| { readonly mode: "one-shot"; readonly label?: string }
|
|
29
|
+
| { readonly mode: "continuable"; readonly label: string };
|
|
30
|
+
|
|
31
|
+
/** Parent catalog v0 records local modes, v1 adds unknown children, and v2 records external executions. */
|
|
32
|
+
export type SubagentCatalogEvent = {
|
|
33
|
+
readonly childId: SessionId;
|
|
34
|
+
readonly childCreatedAt: number;
|
|
35
|
+
} & (
|
|
36
|
+
| ({ readonly version: 0 } & KnownCatalogMode)
|
|
37
|
+
| ({ readonly version: 1 } & (
|
|
38
|
+
| KnownCatalogMode
|
|
39
|
+
| { readonly mode: "unknown"; readonly label?: string }
|
|
40
|
+
))
|
|
41
|
+
| { readonly version: 2; readonly mode: "external"; readonly label?: string }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
declare module "@deepseek-ai/dsh-session/types" {
|
|
45
|
+
interface SessionEventMap {
|
|
46
|
+
/**
|
|
47
|
+
* A direct child's identity and available discovery fields.
|
|
48
|
+
* @param data - versioned parent-owned catalog entry.
|
|
49
|
+
*/
|
|
50
|
+
"subagent/catalog": SubagentCatalogEvent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Host fold state for one parent catalog. */
|
|
55
|
+
export interface SubagentCatalogState {
|
|
56
|
+
readonly inheritedEventCount: SessionLogOffset;
|
|
57
|
+
readonly head?: ChunkedList<SubagentCatalogEvent> | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sessionIdSchema = z.string() as unknown as z.ZodType<SessionId>;
|
|
61
|
+
const oneShotCatalogSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
version: z.union([z.literal(0), z.literal(1)]),
|
|
64
|
+
childId: sessionIdSchema,
|
|
65
|
+
childCreatedAt: z.number().int().nonnegative(),
|
|
66
|
+
mode: z.literal("one-shot"),
|
|
67
|
+
label: z.string().optional(),
|
|
68
|
+
})
|
|
69
|
+
.strict();
|
|
70
|
+
const continuableCatalogSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
version: z.union([z.literal(0), z.literal(1)]),
|
|
73
|
+
childId: sessionIdSchema,
|
|
74
|
+
childCreatedAt: z.number().int().nonnegative(),
|
|
75
|
+
mode: z.literal("continuable"),
|
|
76
|
+
label: z.string(),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
const externalCatalogSchema = oneShotCatalogSchema.extend({
|
|
80
|
+
version: z.literal(2),
|
|
81
|
+
mode: z.literal("external"),
|
|
82
|
+
});
|
|
83
|
+
const unknownCatalogSchema = oneShotCatalogSchema.extend({
|
|
84
|
+
version: z.literal(1),
|
|
85
|
+
mode: z.literal("unknown"),
|
|
86
|
+
});
|
|
87
|
+
const eventDataSchema = z.union([
|
|
88
|
+
oneShotCatalogSchema,
|
|
89
|
+
externalCatalogSchema,
|
|
90
|
+
continuableCatalogSchema,
|
|
91
|
+
unknownCatalogSchema,
|
|
92
|
+
]) as z.ZodType<SubagentCatalogEvent>;
|
|
93
|
+
const viewSchema = z.array(
|
|
94
|
+
z.union([
|
|
95
|
+
oneShotCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({
|
|
96
|
+
id: sessionIdSchema,
|
|
97
|
+
createdAt: oneShotCatalogSchema.shape.childCreatedAt,
|
|
98
|
+
}),
|
|
99
|
+
externalCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({
|
|
100
|
+
id: sessionIdSchema,
|
|
101
|
+
createdAt: externalCatalogSchema.shape.childCreatedAt,
|
|
102
|
+
}),
|
|
103
|
+
continuableCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({
|
|
104
|
+
id: sessionIdSchema,
|
|
105
|
+
createdAt: continuableCatalogSchema.shape.childCreatedAt,
|
|
106
|
+
}),
|
|
107
|
+
unknownCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({
|
|
108
|
+
id: sessionIdSchema,
|
|
109
|
+
createdAt: unknownCatalogSchema.shape.childCreatedAt,
|
|
110
|
+
}),
|
|
111
|
+
]),
|
|
112
|
+
) as z.ZodType<SubagentCatalogEntry[]>;
|
|
113
|
+
const stateSchema: z.ZodType<SubagentCatalogState> = z
|
|
114
|
+
.object({
|
|
115
|
+
inheritedEventCount: z.number().int().nonnegative() as unknown as z.ZodType<SessionLogOffset>,
|
|
116
|
+
head: chunkedListSchema(eventDataSchema).optional(),
|
|
117
|
+
})
|
|
118
|
+
.strict();
|
|
119
|
+
|
|
120
|
+
declare module "@deepseek-ai/dsh-session-projection/types" {
|
|
121
|
+
interface SessionProjectionStateMap {
|
|
122
|
+
subagentCatalog: SubagentCatalogState;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Materialize complete and unknown-mode child identities from parent catalog events.
|
|
128
|
+
* @param state - parent catalog fold state.
|
|
129
|
+
* @returns current direct-child rows in parent catalog event order.
|
|
130
|
+
*/
|
|
131
|
+
function subagentCatalogEntries(state: SubagentCatalogState): SubagentCatalogEntry[] {
|
|
132
|
+
const entries: SubagentCatalogEntry[] = [];
|
|
133
|
+
for (const data of iterateChunkedList(state.head)) {
|
|
134
|
+
const { version: _version, childId, childCreatedAt, ...descriptor } = data;
|
|
135
|
+
entries.push({ id: childId, createdAt: childCreatedAt, ...descriptor });
|
|
136
|
+
}
|
|
137
|
+
return entries;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Parent-owned direct-child catalog projection; invalid own facts reject restoration. */
|
|
141
|
+
export const subagentCatalogProjectionDefinition = {
|
|
142
|
+
key: "subagentCatalog",
|
|
143
|
+
stateSchema,
|
|
144
|
+
init: (_header: SessionHeader, inheritedEventCount: SessionLogOffset) => ({
|
|
145
|
+
inheritedEventCount,
|
|
146
|
+
}),
|
|
147
|
+
apply: (state, event: SessionEvent) => {
|
|
148
|
+
if (event.type !== "subagent/catalog" || event.seq < state.inheritedEventCount) return state;
|
|
149
|
+
return { ...state, head: appendChunkedList(state.head, eventDataSchema.parse(event.data)) };
|
|
150
|
+
},
|
|
151
|
+
stateVersion: 6,
|
|
152
|
+
wire: { viewSchema, view: subagentCatalogEntries },
|
|
153
|
+
} satisfies ProjectionDefinition<"subagentCatalog", SubagentCatalogState>;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Append a complete direct-child discovery fact to its parent Session.
|
|
157
|
+
* @param parent - durable direct parent receiving the discovery fact.
|
|
158
|
+
* @param child - established child's immutable Session metadata.
|
|
159
|
+
* @param descriptor - mode-discriminated creation label frozen with the child.
|
|
160
|
+
*/
|
|
161
|
+
export function establishCatalogChild(
|
|
162
|
+
parent: Session,
|
|
163
|
+
child: SessionHeader,
|
|
164
|
+
descriptor: { readonly mode: "continuable"; readonly label: string },
|
|
165
|
+
): void {
|
|
166
|
+
parent.append("subagent/catalog", {
|
|
167
|
+
version: SUBAGENT_CATALOG_VERSION,
|
|
168
|
+
childId: child.id,
|
|
169
|
+
childCreatedAt: child.createdAt,
|
|
170
|
+
mode: descriptor.mode,
|
|
171
|
+
label: descriptor.label,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Catalog payload version emitted by external execution creation. */
|
|
176
|
+
const EXTERNAL_SUBAGENT_CATALOG_VERSION = 2;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Record an external execution without a local child Session.
|
|
180
|
+
* @param parent - durable direct parent receiving the discovery fact.
|
|
181
|
+
* @param childId - identity returned by the external provider.
|
|
182
|
+
* @param label - display label assigned by the caller.
|
|
183
|
+
*/
|
|
184
|
+
export function establishExternalCatalogChild(
|
|
185
|
+
parent: Session,
|
|
186
|
+
childId: SessionId,
|
|
187
|
+
label: string,
|
|
188
|
+
): void {
|
|
189
|
+
parent.append("subagent/catalog", {
|
|
190
|
+
version: EXTERNAL_SUBAGENT_CATALOG_VERSION,
|
|
191
|
+
childId,
|
|
192
|
+
childCreatedAt: Date.now(),
|
|
193
|
+
mode: "external",
|
|
194
|
+
label,
|
|
195
|
+
});
|
|
196
|
+
}
|