@morlay/dsh-subagent 0.0.10 → 0.0.11-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,19 +2,19 @@
2
2
  * @module @deepseek-ai/dsh-subagent/activation
3
3
  */
4
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";
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
14
 
15
15
  /** Process-local slots shared through uninterrupted continuable parent links. */
16
16
  export class ActivationPool {
17
- private readonly slots = new Set<symbol>();
17
+ private readonly slots = new Set<symbol>()
18
18
 
19
19
  /**
20
20
  * Reserve before reconstruction; release also tolerates unpublished rollback.
@@ -24,16 +24,14 @@ export class ActivationPool {
24
24
  reserve(capacity: number): () => void {
25
25
  if (this.slots.size >= capacity) {
26
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
- );
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
31
  }
32
- const slot = Symbol();
33
- this.slots.add(slot);
34
- return () => {
35
- this.slots.delete(slot);
36
- };
32
+ const slot = Symbol()
33
+ this.slots.add(slot)
34
+ return () => { this.slots.delete(slot) }
37
35
  }
38
36
  }
39
37
 
@@ -43,61 +41,67 @@ export class ActivationPool {
43
41
  */
44
42
  interface ActivationBase {
45
43
  /** Shared capacity for this Activation and all its managed descendants. */
46
- readonly pool: ActivationPool;
44
+ readonly pool: ActivationPool
47
45
  /** Return this epoch's slot after its handle has finished disposal. */
48
- readonly releaseSlot: () => void;
46
+ readonly releaseSlot: () => void
49
47
  /** 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>;
48
+ readonly childId: SessionId
49
+ readonly parent: Agent
50
+ readonly delivery: 'parent' | 'caller'
51
+ readonly result: PromiseWithResolvers<SubagentResult>
54
52
  /** Published synchronously before teardown; subsequent callers share this transaction. */
55
- closing: Promise<void> | undefined;
53
+ closing: Promise<void> | undefined
56
54
  /**
57
55
  * Exact live Agent ancestry observed when this Activation materialized.
58
56
  * Weak membership preserves host-scope identity across an intermediate
59
57
  * ancestor leaving the registry without retaining that ancestor's runtime.
60
58
  */
61
- readonly ancestry: WeakSet<Agent>;
59
+ readonly ancestry: WeakSet<Agent>
62
60
  /**
63
61
  * Session ids of the child Activations this one owns. Because one Session has
64
62
  * at most one live Activation, the id identifies the live child without
65
63
  * another runtime-incarnation reference. Non-empty blocks settlement.
66
64
  */
67
- readonly ownedChildren: Set<SessionId>;
65
+ readonly ownedChildren: Set<SessionId>
68
66
  /** The lifecycle observer that emits this epoch's start and terminal edges. */
69
- readonly observer: ActivationObserver;
67
+ readonly observer: ActivationObserver
70
68
  /**
71
69
  * Whether any delivery to this child was ever accepted. A materialization
72
70
  * rolled back before its first acceptance is a child the caller was told does
73
71
  * not exist, so its teardown owes the parent no settlement account.
74
72
  */
75
- announced: boolean;
73
+ announced: boolean
76
74
  }
77
75
 
78
76
  /** Resources retained only for the selected execution kind. */
79
77
  export type ActivationResource =
80
78
  | {
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
- }
79
+ readonly kind: 'local'
80
+ readonly handle: AgentHandle
81
+ readonly outputStart: SessionLogOffset
82
+ readonly structured: StructuredAttachment | undefined
83
+ /** Latest live failure not superseded by a later committed turn ending. */
84
+ failureAt: SessionLogOffset | undefined
85
+ /** Renewed when local settlement must recheck input or owned children. */
86
+ poke: PromiseWithResolvers<void>
87
+ /**
88
+ * Whether this child's settlement notice omits its closing text: true while
89
+ * the child's last logged tool call was a message to its direct parent and
90
+ * no input was claimed since.
91
+ */
92
+ omitClosingText: boolean
93
+ }
90
94
  | {
91
- readonly kind: "external";
92
- readonly run: SubagentRun;
93
- readonly controller: AbortController;
94
- };
95
+ readonly kind: 'external'
96
+ readonly run: SubagentRun
97
+ readonly controller: AbortController
98
+ }
95
99
 
96
100
  /** One managed execution period with its provider-specific resources. */
97
- export type Activation = ActivationBase & ActivationResource;
101
+ export type Activation = ActivationBase & ActivationResource
98
102
 
99
103
  /** Local residency with input admission and Session-backed result capture. */
100
- export type LocalActivation = Extract<Activation, { kind: "local" }>;
104
+ export type LocalActivation = Extract<Activation, { kind: 'local' }>
101
105
 
102
106
  /**
103
107
  * Read the live Agent retained by local execution.
@@ -105,7 +109,7 @@ export type LocalActivation = Extract<Activation, { kind: "local" }>;
105
109
  * @returns the local Agent, absent for an external run.
106
110
  */
107
111
  export function activationAgent(activation: Activation): Agent | undefined {
108
- return activation.kind === "local" ? activation.handle.agent : undefined;
112
+ return activation.kind === 'local' ? activation.handle.agent : undefined
109
113
  }
110
114
 
111
115
  /**
@@ -116,16 +120,11 @@ export function activationAgent(activation: Activation): Agent | undefined {
116
120
  */
117
121
  export function requireLocalActivation(activation: Activation): LocalActivation {
118
122
  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
- );
123
+ case 'local': return activation
124
+ case 'external':
125
+ throw new SubagentError(`subagent "${activation.childId}" does not accept follow-up input`, 'NOT_CONTINUABLE')
126
126
  /* v8 ignore next 2 -- Both execution kinds are handled above. */
127
- default:
128
- return assertNever(activation);
127
+ default: return assertNever(activation)
129
128
  }
130
129
  }
131
130
 
@@ -135,22 +134,18 @@ export function requireLocalActivation(activation: Activation): LocalActivation
135
134
  * @param wake - notify its settlement watcher when failure changes readiness.
136
135
  */
137
136
  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;
137
+ const child = activation.handle.agent
138
+ child.ctx.on('agent/error', ({ agent }) => {
139
+ if (agent !== child) return
140
+ activation.failureAt = child.session.seq
141
+ wake()
142
+ })
143
+ child.ctx.on('session/event', (session, event) => {
144
+ if (session === child.session && event.type === 'turn/end'
145
+ && activation.failureAt !== undefined && event.seq >= activation.failureAt) {
146
+ activation.failureAt = undefined
152
147
  }
153
- });
148
+ })
154
149
  }
155
150
 
156
151
  /**
@@ -160,21 +155,21 @@ export function observeLocalFailure(activation: LocalActivation, wake: () => voi
160
155
  */
161
156
  export function captureLocalResult(activation: LocalActivation): SubagentResult {
162
157
  // 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;
158
+ const events = activation.handle.agent.session.snapshotEvents(activation.outputStart)
159
+ const output = finalAssistantOutput(events) ?? []
160
+ const stopReason = activation.failureAt === undefined ? epochStopReason(events) : 'error'
161
+ const structured = activation.structured
167
162
  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" };
163
+ const captured = structured.captured()
164
+ if (captured !== undefined) return { output, stopReason, structured: captured.value }
165
+ if (stopReason === 'completed') return { output, stopReason: 'error' }
171
166
  }
172
- return { output, stopReason };
167
+ return { output, stopReason }
173
168
  }
174
169
 
175
170
  /** Serialize each durable child's delivery, release, and disposal. */
176
171
  export class ChildLock {
177
- private tails = new Map<SessionId, Promise<unknown>>();
172
+ private tails = new Map<SessionId, Promise<unknown>>()
178
173
 
179
174
  /**
180
175
  * Run `operation` after every previously queued operation for `childId`.
@@ -183,18 +178,15 @@ export class ChildLock {
183
178
  * @returns the operation's own settlement.
184
179
  */
185
180
  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);
181
+ const previous = this.tails.get(childId) ?? Promise.resolve()
182
+ const result = previous.then(operation, operation)
188
183
  // Absorb rejections in the chaining tail so one failed critical section
189
184
  // cannot reject an unrelated later caller.
190
- const tail = result.then(
191
- () => undefined,
192
- () => undefined,
193
- );
194
- this.tails.set(childId, tail);
185
+ const tail = result.then(() => undefined, () => undefined)
186
+ this.tails.set(childId, tail)
195
187
  void tail.then(() => {
196
- if (this.tails.get(childId) === tail) this.tails.delete(childId);
197
- });
198
- return result;
188
+ if (this.tails.get(childId) === tail) this.tails.delete(childId)
189
+ })
190
+ return result
199
191
  }
200
192
  }
@@ -6,12 +6,12 @@
6
6
  * @module @deepseek-ai/dsh-subagent
7
7
  */
8
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";
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
15
 
16
16
  /**
17
17
  * Answer `workspace/session-activity` with the running subagent descendants of
@@ -20,28 +20,23 @@ import { foldSubagentDescriptor } from "./descriptor.ts";
20
20
  * @param ctx - context carrying the Agent registry; the Session query service is optional and only supplies labels.
21
21
  */
22
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 }) => {
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 = { kind: 'subagent', items: await Promise.all(running.map(child => describe(ctx, child))) }
28
+ return [own, ...rest]
29
+ })
30
+ ctx.on('workspace/session-stop', ({ sessionId }) => {
34
31
  for (const child of runningDescendants(ctx, sessionId)) {
35
32
  try {
36
- child.cancel({ kind: "parent" });
33
+ child.cancel({ kind: 'parent' })
37
34
  } catch (error: unknown) {
38
35
  // 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
- );
36
+ ctx.logger.warn(`subagent: cancelling "${child.id}" for an archived Session failed: ${String(error)}`)
42
37
  }
43
38
  }
44
- });
39
+ })
45
40
  }
46
41
 
47
42
  /**
@@ -52,27 +47,27 @@ export function installSubagentArchiveAdmission(ctx: Context): void {
52
47
  * is read as data, so a damaged header chain that loops is visited once.
53
48
  */
54
49
  function runningDescendants(ctx: Context, rootId: SessionId): Agent[] {
55
- const childrenOf = new Map<SessionId, Agent[]>();
50
+ const childrenOf = new Map<SessionId, Agent[]>()
56
51
  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);
52
+ const { parentSession, origin } = agent.session.header
53
+ if (parentSession === undefined || origin !== 'subagent') continue
54
+ const siblings = childrenOf.get(parentSession) ?? []
55
+ siblings.push(agent)
56
+ childrenOf.set(parentSession, siblings)
62
57
  }
63
- const running: Agent[] = [];
64
- const pending = [rootId];
65
- const visited = new Set<SessionId>();
58
+ const running: Agent[] = []
59
+ const pending = [rootId]
60
+ const visited = new Set<SessionId>()
66
61
  while (pending.length > 0) {
67
- const parentId = pending.shift() as SessionId;
68
- if (visited.has(parentId)) continue;
69
- visited.add(parentId);
62
+ const parentId = pending.shift() as SessionId
63
+ if (visited.has(parentId)) continue
64
+ visited.add(parentId)
70
65
  for (const child of childrenOf.get(parentId) ?? []) {
71
- if (child.status === "running") running.push(child);
72
- pending.push(child.id);
66
+ if (child.status === 'running') running.push(child)
67
+ pending.push(child.id)
73
68
  }
74
69
  }
75
- return running;
70
+ return running
76
71
  }
77
72
 
78
73
  /**
@@ -82,16 +77,14 @@ function runningDescendants(ctx: Context, rootId: SessionId): Agent[] {
82
77
  * the item names the child by id alone.
83
78
  */
84
79
  async function describe(ctx: Context, child: Agent): Promise<SessionActivityItem> {
85
- const query = ctx.get("sessionQuery");
86
- if (query === undefined) return { id: child.id };
80
+ const query = ctx.get('sessionQuery')
81
+ if (query === undefined) return { id: child.id }
87
82
  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 };
83
+ using observation = await query.observeSession(child.id, { projectionMode: 'none' })
84
+ const label = foldSubagentDescriptor(observation.events.slice(observation.inheritedEventCount))?.label
85
+ return label === undefined ? { id: child.id } : { id: child.id, label }
93
86
  } catch {
94
87
  // A damaged descriptor is data damage in the child; the report still names the child.
95
- return { id: child.id };
88
+ return { id: child.id }
96
89
  }
97
90
  }
@@ -10,8 +10,8 @@
10
10
  * @module @deepseek-ai/dsh-subagent/assistant-output
11
11
  */
12
12
 
13
- import { joinAssistantStreamText, type ContentBlock } from "@deepseek-ai/dsh-llm";
14
- import type { SessionEvent } from "@deepseek-ai/dsh-session";
13
+ import { joinAssistantStreamText, type ContentBlock } from '@deepseek-ai/dsh-llm'
14
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
15
15
 
16
16
  /**
17
17
  * Incremental fold of the selection rule, for backends that observe a child's
@@ -20,8 +20,8 @@ import type { SessionEvent } from "@deepseek-ai/dsh-session";
20
20
  * text into the same streamed fallback.
21
21
  */
22
22
  export class AssistantOutputFold {
23
- private message: readonly ContentBlock[] | undefined;
24
- private partial: string[] = [];
23
+ private message: readonly ContentBlock[] | undefined
24
+ private partial: string[] = []
25
25
 
26
26
  /**
27
27
  * Fold one session event: a non-empty assistant message becomes the
@@ -30,12 +30,12 @@ export class AssistantOutputFold {
30
30
  * @param event - the next observed session event.
31
31
  */
32
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;
33
+ if (event.type === 'assistant/message') {
34
+ const content = event.data.message.content
35
+ if (content.length > 0) this.message = content
36
36
  }
37
- if (event.type === "assistant/message" || event.type === "assistant/attempt") {
38
- this.pushText(joinAssistantStreamText(event.data.stream));
37
+ if (event.type === 'assistant/message' || event.type === 'assistant/attempt') {
38
+ this.pushText(joinAssistantStreamText(event.data.stream))
39
39
  }
40
40
  }
41
41
 
@@ -44,7 +44,7 @@ export class AssistantOutputFold {
44
44
  * @param text - the next streamed text piece (an empty piece is a no-op).
45
45
  */
46
46
  pushText(text: string): void {
47
- if (text.length > 0) this.partial.push(text);
47
+ if (text.length > 0) this.partial.push(text)
48
48
  }
49
49
 
50
50
  /**
@@ -53,9 +53,9 @@ export class AssistantOutputFold {
53
53
  * streamed text, or `undefined` when the child produced neither.
54
54
  */
55
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;
56
+ if (this.message !== undefined) return this.message
57
+ const text = this.partial.join('')
58
+ return text.length > 0 ? [{ type: 'text', text }] : undefined
59
59
  }
60
60
  }
61
61
 
@@ -64,14 +64,12 @@ export class AssistantOutputFold {
64
64
  * @param events - the child-owned events (after any seed or epoch boundary).
65
65
  * @returns the selected output, or `undefined` when the child produced none.
66
66
  */
67
- export function finalAssistantOutput(
68
- events: readonly SessionEvent[],
69
- ): readonly ContentBlock[] | undefined {
67
+ export function finalAssistantOutput(events: readonly SessionEvent[]): readonly ContentBlock[] | undefined {
70
68
  // TODO: this folds the complete suffix once per run/epoch settlement. If a
71
69
  // long continuable epoch ever profiles hot here, scan backward with early
72
70
  // exit for the last non-empty message and fold text deltas only on the
73
71
  // no-message fallback.
74
- const fold = new AssistantOutputFold();
75
- for (const event of events) fold.push(event);
76
- return fold.collect();
72
+ const fold = new AssistantOutputFold()
73
+ for (const event of events) fold.push(event)
74
+ return fold.collect()
77
75
  }