@qlzt/dsh-subagent 0.0.0-stage → 0.2.1-alpha.3

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +94 -0
  3. package/README.md +215 -2
  4. package/README.zh.md +216 -0
  5. package/lib/index.js +3333 -0
  6. package/lib/typert.host.d.ts +3 -0
  7. package/lib/typert.host.js +991 -0
  8. package/lib/typert.remote-client.d.ts +25 -0
  9. package/lib/typert.remote-client.js +114 -0
  10. package/lib/types/activation.d.ts +114 -0
  11. package/lib/types/activation.js +112 -0
  12. package/lib/types/archive-admission.d.ts +16 -0
  13. package/lib/types/archive-admission.js +151 -0
  14. package/lib/types/assistant-output.d.ts +48 -0
  15. package/lib/types/assistant-output.js +73 -0
  16. package/lib/types/catalog.d.ts +92 -0
  17. package/lib/types/catalog.js +113 -0
  18. package/lib/types/child-agent.d.ts +154 -0
  19. package/lib/types/child-agent.js +211 -0
  20. package/lib/types/client.d.ts +8 -0
  21. package/lib/types/client.js +7 -0
  22. package/lib/types/continuation-messages.d.ts +63 -0
  23. package/lib/types/continuation-messages.js +114 -0
  24. package/lib/types/control-types.d.ts +142 -0
  25. package/lib/types/control-types.js +8 -0
  26. package/lib/types/control.d.ts +45 -0
  27. package/lib/types/control.js +79 -0
  28. package/lib/types/depth.d.ts +31 -0
  29. package/lib/types/depth.js +39 -0
  30. package/lib/types/descriptor.d.ts +105 -0
  31. package/lib/types/descriptor.js +192 -0
  32. package/lib/types/error.d.ts +11 -0
  33. package/lib/types/error.js +14 -0
  34. package/lib/types/index.d.ts +319 -0
  35. package/lib/types/index.js +455 -0
  36. package/lib/types/internal.d.ts +58 -0
  37. package/lib/types/internal.js +58 -0
  38. package/lib/types/lifecycle.d.ts +83 -0
  39. package/lib/types/lifecycle.js +115 -0
  40. package/lib/types/list-children.d.ts +36 -0
  41. package/lib/types/list-children.js +166 -0
  42. package/lib/types/manager.d.ts +247 -0
  43. package/lib/types/manager.js +1294 -0
  44. package/lib/types/out-of-process.d.ts +92 -0
  45. package/lib/types/out-of-process.js +178 -0
  46. package/lib/types/projection-types.d.ts +85 -0
  47. package/lib/types/projection-types.js +7 -0
  48. package/lib/types/projection.d.ts +100 -0
  49. package/lib/types/projection.js +157 -0
  50. package/lib/types/structured.d.ts +49 -0
  51. package/lib/types/structured.js +130 -0
  52. package/lib/types/types.d.ts +353 -0
  53. package/lib/types/types.js +19 -0
  54. package/package.json +136 -4
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Provider-side vocabulary for OUT-OF-PROCESS subagent backends — the pieces
3
+ * that enforce this seam's own contracts around a child in another process:
4
+ * the no-capabilities advertisement, timing-bound validation, child
5
+ * working-directory validation, the never-reject result settlement, and the standard
6
+ * run-handle publication. Backends compose these with their own wire drivers;
7
+ * the process machinery itself (spawn, env scrub, managed-range teardown)
8
+ * belongs to the `dsh-subprocess` seam.
9
+ *
10
+ * @module @qlzt/dsh-subagent/out-of-process
11
+ */
12
+ import type { ContentBlock } from '@qlzt/dsh-llm';
13
+ import type { SubagentCapabilities, SubagentResult, SubagentRun, SubagentStopReason } from './types.ts';
14
+ /**
15
+ * The capability advertisement of an out-of-process backend: NONE. A child in
16
+ * another process cannot honor parent-enforced start features
17
+ * (`agentOptions`/`outputSchema`/`maxDepth`/`toolFilter`/`persona`), so the service rejects a
18
+ * request needing any of them before `start` runs — never accepted-then-ignored.
19
+ */
20
+ export declare const NO_START_CAPABILITIES: SubagentCapabilities;
21
+ /**
22
+ * Assert a configured timing bound is a positive finite number (it bounds a
23
+ * teardown or shutdown wait; zero, negative, or NaN would skip or wedge it).
24
+ * @param prefix - the consuming plugin's diagnostic prefix (e.g. `subagent-acp`).
25
+ * @param name - the config field name, for the diagnostic.
26
+ * @param value - the configured value.
27
+ */
28
+ export declare function assertPositiveFinite(prefix: string, name: string, value: number): void;
29
+ /**
30
+ * Assert `cwd` can actually host the child: absolute (it doubles as the
31
+ * child's workspace identity, and a relative path would be re-anchored to the
32
+ * server process's launch directory) and an existing directory (fail here,
33
+ * before the process boundary, instead of as an ambiguous spawn ENOENT).
34
+ * @param prefix - the consuming plugin's diagnostic prefix.
35
+ * @param label - which source supplied the value, for the diagnostic.
36
+ * @param cwd - the candidate working directory.
37
+ * @returns `cwd`, validated.
38
+ */
39
+ export declare function assertUsableCwd(prefix: string, label: string, cwd: string): string;
40
+ /** Inputs to {@link settleRunResult}. */
41
+ export interface RunResultSettlement {
42
+ /** The turn attempt (typically racing local cancellation); returns the terminal result. */
43
+ attempt: () => Promise<SubagentResult>;
44
+ /** Snapshot the provider exposes when cancellation or failure wins settlement. */
45
+ collectOutput: () => readonly ContentBlock[];
46
+ /** Snapshot safe provider-authored detail when a failure wins settlement. */
47
+ collectDiagnostic?: (() => string | undefined) | undefined;
48
+ /** Whether local cancellation settled before the attempt's outcome is observed. */
49
+ cancelled: () => boolean;
50
+ /** Diagnostic sink for a failure flattened to a stop reason; a throw from it is contained. */
51
+ onError?: ((error: Error, stopReason: SubagentStopReason) => void) | undefined;
52
+ /** The request's cancellation signal (the listener is removed at settlement). */
53
+ signal: AbortSignal;
54
+ /** The abort listener registered on {@link signal} at start. */
55
+ onAbort: () => void;
56
+ }
57
+ /**
58
+ * Settle an out-of-process run result under the seam contract: `result` never
59
+ * rejects after publication. A normally completed or rejected attempt resolves
60
+ * as `aborted` when cancellation already settled locally; another rejection is
61
+ * flattened to `stopReason: 'error'` through the contained diagnostic sink.
62
+ * Provider-returned diagnostics use the same byte limit. The abort listener is
63
+ * removed on every path.
64
+ * @param parts - the attempt, output snapshot, cancellation state, sink, and signal wiring.
65
+ * @returns the terminal result (never a rejection).
66
+ */
67
+ export declare function settleRunResult(parts: RunResultSettlement): Promise<SubagentResult>;
68
+ /** Inputs to {@link subprocessRunHandle}. */
69
+ export interface SubprocessRunHandleParts {
70
+ /** The parent-scoped run id. */
71
+ id: SubagentRun['id'];
72
+ /** The flattened, never-rejecting result (the seam contract). */
73
+ result: Promise<SubagentResult>;
74
+ /** The request's cancellation signal (the listener is removed on dispose). */
75
+ signal: AbortSignal;
76
+ /** The abort listener registered on {@link signal} at start. */
77
+ onAbort: () => void;
78
+ /** Settle local cancellation so {@link result} resolves without the child. */
79
+ requestCancel: () => void;
80
+ /** Tear the child process down to quiescence (backend-owned ladder). */
81
+ teardown: () => Promise<void>;
82
+ }
83
+ /**
84
+ * Publish the seam run handle for an out-of-process child. `dispose()` is
85
+ * idempotent (one memoized teardown): it removes the abort listener, settles
86
+ * local cancellation — there is no assumption the child cooperates — and then
87
+ * awaits the backend's teardown to actual exit.
88
+ * @param parts - the run identity, result, cancellation wiring, and teardown.
89
+ * @returns the external run handle.
90
+ */
91
+ export declare function subprocessRunHandle(parts: SubprocessRunHandleParts): SubagentRun;
92
+ //# sourceMappingURL=out-of-process.d.ts.map
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Provider-side vocabulary for OUT-OF-PROCESS subagent backends — the pieces
3
+ * that enforce this seam's own contracts around a child in another process:
4
+ * the no-capabilities advertisement, timing-bound validation, child
5
+ * working-directory validation, the never-reject result settlement, and the standard
6
+ * run-handle publication. Backends compose these with their own wire drivers;
7
+ * the process machinery itself (spawn, env scrub, managed-range teardown)
8
+ * belongs to the `dsh-subprocess` seam.
9
+ *
10
+ * @module @qlzt/dsh-subagent/out-of-process
11
+ */
12
+ import { accessSync, constants, statSync } from 'node:fs';
13
+ import { isAbsolute } from 'node:path';
14
+ /** Maximum UTF-8 size of {@link SubagentResult.diagnostic}. */
15
+ const MAX_SUBAGENT_DIAGNOSTIC_BYTES = 4_096;
16
+ const DIAGNOSTIC_TRUNCATION_SUFFIX = '\n[diagnostic truncated]';
17
+ const utf8Encoder = new TextEncoder();
18
+ const utf8Decoder = new TextDecoder();
19
+ /**
20
+ * Limit provider-authored failure detail without splitting a UTF-8 sequence.
21
+ * @param diagnostic - safe diagnostic text produced by the provider.
22
+ * @returns the original text, or a visibly truncated value within the limit.
23
+ */
24
+ function limitSubagentDiagnostic(diagnostic) {
25
+ const bytes = utf8Encoder.encode(diagnostic);
26
+ if (bytes.byteLength <= MAX_SUBAGENT_DIAGNOSTIC_BYTES)
27
+ return diagnostic;
28
+ const suffixBytes = utf8Encoder.encode(DIAGNOSTIC_TRUNCATION_SUFFIX).byteLength;
29
+ let prefixBytes = MAX_SUBAGENT_DIAGNOSTIC_BYTES - suffixBytes;
30
+ while ((bytes[prefixBytes] & 0b1100_0000) === 0b1000_0000) {
31
+ prefixBytes -= 1;
32
+ }
33
+ return utf8Decoder.decode(bytes.subarray(0, prefixBytes))
34
+ + DIAGNOSTIC_TRUNCATION_SUFFIX;
35
+ }
36
+ /** Enforce the byte limit on a provider-returned diagnostic. */
37
+ function normalizeSubagentDiagnostic(result) {
38
+ return result.diagnostic === undefined
39
+ ? result
40
+ : { ...result, diagnostic: limitSubagentDiagnostic(result.diagnostic) };
41
+ }
42
+ /**
43
+ * The capability advertisement of an out-of-process backend: NONE. A child in
44
+ * another process cannot honor parent-enforced start features
45
+ * (`agentOptions`/`outputSchema`/`maxDepth`/`toolFilter`/`persona`), so the service rejects a
46
+ * request needing any of them before `start` runs — never accepted-then-ignored.
47
+ */
48
+ export const NO_START_CAPABILITIES = Object.freeze({
49
+ agentOptions: false,
50
+ outputSchema: false,
51
+ depthLimit: false,
52
+ toolFilter: false,
53
+ persona: false,
54
+ });
55
+ /**
56
+ * Assert a configured timing bound is a positive finite number (it bounds a
57
+ * teardown or shutdown wait; zero, negative, or NaN would skip or wedge it).
58
+ * @param prefix - the consuming plugin's diagnostic prefix (e.g. `subagent-acp`).
59
+ * @param name - the config field name, for the diagnostic.
60
+ * @param value - the configured value.
61
+ */
62
+ export function assertPositiveFinite(prefix, name, value) {
63
+ if (!Number.isFinite(value) || value <= 0) {
64
+ throw new Error(`${prefix}: ${name} must be a positive finite number`);
65
+ }
66
+ }
67
+ /**
68
+ * Whether `path` names an existing directory the harness can ENTER. The
69
+ * search-permission probe matters: `statSync().isDirectory()` is true for a
70
+ * mode-600 directory, but a subprocess cwd needs `X_OK` or spawn fails EACCES.
71
+ */
72
+ function isEnterableDirectory(path) {
73
+ try {
74
+ if (!statSync(path).isDirectory())
75
+ return false;
76
+ accessSync(path, constants.X_OK);
77
+ return true;
78
+ }
79
+ catch {
80
+ // statSync/accessSync throw only filesystem access errors here
81
+ // (ENOENT/EACCES/ENOTDIR/…), and every one of them means the path cannot
82
+ // serve as the child's cwd.
83
+ return false;
84
+ }
85
+ }
86
+ /**
87
+ * Assert `cwd` can actually host the child: absolute (it doubles as the
88
+ * child's workspace identity, and a relative path would be re-anchored to the
89
+ * server process's launch directory) and an existing directory (fail here,
90
+ * before the process boundary, instead of as an ambiguous spawn ENOENT).
91
+ * @param prefix - the consuming plugin's diagnostic prefix.
92
+ * @param label - which source supplied the value, for the diagnostic.
93
+ * @param cwd - the candidate working directory.
94
+ * @returns `cwd`, validated.
95
+ */
96
+ export function assertUsableCwd(prefix, label, cwd) {
97
+ if (!isAbsolute(cwd)) {
98
+ throw new Error(`${prefix}: ${label} must be an absolute path: ${cwd}`);
99
+ }
100
+ if (!isEnterableDirectory(cwd)) {
101
+ throw new Error(`${prefix}: ${label} is not an accessible directory: ${cwd}`);
102
+ }
103
+ return cwd;
104
+ }
105
+ /** Normalize an unknown thrown value to an Error (the catch binding is `unknown`). */
106
+ function toError(value) {
107
+ // The rejecting surfaces (wire clients, spawn failures) only throw
108
+ // `Error`s; the `String(value)` arm is a defensive fallback for a non-Error
109
+ // throw the typed surfaces cannot produce.
110
+ /* v8 ignore next */
111
+ return value instanceof Error ? value : new Error(String(value));
112
+ }
113
+ /**
114
+ * Settle an out-of-process run result under the seam contract: `result` never
115
+ * rejects after publication. A normally completed or rejected attempt resolves
116
+ * as `aborted` when cancellation already settled locally; another rejection is
117
+ * flattened to `stopReason: 'error'` through the contained diagnostic sink.
118
+ * Provider-returned diagnostics use the same byte limit. The abort listener is
119
+ * removed on every path.
120
+ * @param parts - the attempt, output snapshot, cancellation state, sink, and signal wiring.
121
+ * @returns the terminal result (never a rejection).
122
+ */
123
+ export async function settleRunResult(parts) {
124
+ try {
125
+ const result = await parts.attempt();
126
+ return parts.cancelled()
127
+ ? { output: parts.collectOutput(), stopReason: 'aborted' }
128
+ : normalizeSubagentDiagnostic(result);
129
+ }
130
+ catch (error) {
131
+ // Cover a rejection already queued when cancellation arrives.
132
+ if (parts.cancelled())
133
+ return { output: parts.collectOutput(), stopReason: 'aborted' };
134
+ // Flatten post-publication transport failures while preserving diagnostics.
135
+ try {
136
+ parts.onError?.(toError(error), 'error');
137
+ }
138
+ catch {
139
+ // The diagnostic sink cannot reject the run result.
140
+ }
141
+ const collected = parts.collectDiagnostic?.();
142
+ const diagnostic = collected === undefined
143
+ ? undefined
144
+ : limitSubagentDiagnostic(collected);
145
+ return {
146
+ output: parts.collectOutput(),
147
+ ...diagnostic === undefined ? {} : { diagnostic },
148
+ stopReason: 'error',
149
+ };
150
+ }
151
+ finally {
152
+ parts.signal.removeEventListener('abort', parts.onAbort);
153
+ }
154
+ }
155
+ /**
156
+ * Publish the seam run handle for an out-of-process child. `dispose()` is
157
+ * idempotent (one memoized teardown): it removes the abort listener, settles
158
+ * local cancellation — there is no assumption the child cooperates — and then
159
+ * awaits the backend's teardown to actual exit.
160
+ * @param parts - the run identity, result, cancellation wiring, and teardown.
161
+ * @returns the external run handle.
162
+ */
163
+ export function subprocessRunHandle(parts) {
164
+ let disposal;
165
+ return {
166
+ id: parts.id,
167
+ result: parts.result,
168
+ dispose() {
169
+ if (disposal !== undefined)
170
+ return disposal;
171
+ parts.signal.removeEventListener('abort', parts.onAbort);
172
+ parts.requestCancel();
173
+ disposal = parts.teardown();
174
+ return disposal;
175
+ },
176
+ };
177
+ }
178
+ //# sourceMappingURL=out-of-process.js.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Pure client-safe subagent projection vocabulary.
3
+ *
4
+ * @module @qlzt/dsh-subagent/projection-types
5
+ */
6
+ import type { SessionId, SessionSeq } from '@qlzt/dsh-session/types';
7
+ /** One current direct-child discovery row materialized from parent facts. */
8
+ export type SubagentCatalogEntry = {
9
+ readonly id: SessionId;
10
+ readonly createdAt: number;
11
+ } & ({
12
+ readonly mode: 'one-shot';
13
+ readonly label?: string;
14
+ } | {
15
+ readonly mode: 'external';
16
+ readonly label?: string;
17
+ } | {
18
+ readonly mode: 'continuable';
19
+ readonly label: string;
20
+ } | {
21
+ readonly mode: 'unknown';
22
+ readonly label?: string;
23
+ });
24
+ /** Durable active-turn timing for one descriptor-backed child session. */
25
+ export interface SubagentTimingProjection {
26
+ /** Milliseconds accumulated across completed turns after the child's own descriptor. */
27
+ settledMs: number;
28
+ /** Same-cut bounds of the currently open turn, when one has not reached `turn/end`. */
29
+ active?: {
30
+ /** Start of the open turn. */
31
+ since: number;
32
+ /** Latest event time folded into this projection cut. */
33
+ through: number;
34
+ };
35
+ /**
36
+ * Whether the latest closed turn after the child's own descriptor completed
37
+ * normally; absent while a turn is open or before one closes.
38
+ */
39
+ lastTurnCompleted?: boolean;
40
+ }
41
+ /**
42
+ * Durable identity of one descriptor-backed subagent session: lifecycle mode
43
+ * plus creation label, folded last-wins from `subagent/descriptor` events.
44
+ * Label strength follows the descriptor schema: a continuable child always
45
+ * carries one, a one-shot child may omit it.
46
+ */
47
+ export type SubagentIdentityProjection = {
48
+ /** A terminal one-shot child. */
49
+ mode: 'one-shot';
50
+ /** Optional durable creation label from the child's descriptor. */
51
+ label?: string;
52
+ /**
53
+ * Seq of the `subagent/descriptor` event this identity was folded from.
54
+ * `session.isOwnSeq(seq)` proves the identity comes from the child's
55
+ * OWN log suffix — where a descriptor is immutable once appended — and
56
+ * not from a fork seed's replayed ancestor descriptor.
57
+ */
58
+ seq: SessionSeq;
59
+ } | {
60
+ /** A resumable conversation. */
61
+ mode: 'continuable';
62
+ /** Durable creation label from the child's descriptor. */
63
+ label: string;
64
+ /** Seq of the folded descriptor event; see the one-shot arm for the own-suffix proof. */
65
+ seq: SessionSeq;
66
+ };
67
+ declare module '@qlzt/dsh-session-projection/types' {
68
+ interface SessionProjectionMap {
69
+ /** Direct children in parent catalog event order, excluding fork-inherited facts. */
70
+ subagentCatalog: SubagentCatalogEntry[];
71
+ /** Active-turn duration and latest closed-turn completion for a descriptor-backed subagent session. */
72
+ subagentTiming: SubagentTimingProjection;
73
+ /**
74
+ * Identity of a descriptor-backed subagent session. `null` ⟺ no valid
75
+ * descriptor (missing, malformed, or unrecognized-version — deliberately
76
+ * undistinguished). The sentinel is deliberately serializable: a
77
+ * value pushed over JSON transports must survive `JSON.stringify`
78
+ * losslessly, where an `undefined` field would be dropped and a stale
79
+ * identity would survive on the receiving side. The entry itself stays
80
+ * non-optional.
81
+ */
82
+ subagent: SubagentIdentityProjection | null;
83
+ }
84
+ }
85
+ //# sourceMappingURL=projection-types.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pure client-safe subagent projection vocabulary.
3
+ *
4
+ * @module @qlzt/dsh-subagent/projection-types
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=projection-types.js.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Pure session projections for subagent identity (mode/label) and active-turn
3
+ * duration.
4
+ *
5
+ * @module @qlzt/dsh-subagent/projection
6
+ */
7
+ import { z } from 'zod';
8
+ import type { SessionEvent } from '@qlzt/dsh-session';
9
+ import type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts';
10
+ /** Fold state for a subagent's latest timing snapshot. */
11
+ export interface TimingState {
12
+ /** Milliseconds accumulated across completed post-descriptor turns. */
13
+ settledMs: number;
14
+ /** Current open interval kept paired inside the fold. */
15
+ active?: {
16
+ since: number;
17
+ through: number;
18
+ } | undefined;
19
+ /** Latest pre-descriptor turn start, promoted when the child's own descriptor arrives. */
20
+ pendingTurnStart?: number | undefined;
21
+ /** Whether the fold has crossed a descriptor in this logical log. */
22
+ descriptorSeen: boolean;
23
+ /** Whether the latest closed post-descriptor turn completed normally; absent while a turn is open or before one closes. */
24
+ lastTurnCompleted?: boolean | undefined;
25
+ }
26
+ declare module '@qlzt/dsh-session-projection/types' {
27
+ interface SessionProjectionStateMap {
28
+ subagentTiming: TimingState;
29
+ subagent: IdentityState;
30
+ }
31
+ }
32
+ /**
33
+ * Fold turn boundaries around the child's own durable descriptor.
34
+ *
35
+ * A fork seed may contain an ancestor descriptor and completed turns. Every
36
+ * descriptor therefore resets the accumulated state; the healthy catalog
37
+ * admits only a child with exactly one descriptor in its own suffix, making
38
+ * the final reset the child's authoritative timing origin.
39
+ */
40
+ export declare const subagentTimingProjectionDefinition: {
41
+ key: "subagentTiming";
42
+ stateSchema: z.ZodType<TimingState, unknown, z.core.$ZodTypeInternals<TimingState, unknown>>;
43
+ init: () => {
44
+ descriptorSeen: false;
45
+ settledMs: number;
46
+ };
47
+ apply: (state: NoInfer<TimingState>, event: SessionEvent) => {
48
+ /** Milliseconds accumulated across completed post-descriptor turns. */
49
+ settledMs: number;
50
+ /** Current open interval kept paired inside the fold. */
51
+ active?: {
52
+ since: number;
53
+ through: number;
54
+ } | undefined;
55
+ /** Whether the fold has crossed a descriptor in this logical log. */
56
+ descriptorSeen: boolean;
57
+ /** Whether the latest closed post-descriptor turn completed normally; absent while a turn is open or before one closes. */
58
+ lastTurnCompleted?: boolean | undefined;
59
+ };
60
+ wire: {
61
+ viewSchema: z.ZodType<SubagentTimingProjection, unknown, z.core.$ZodTypeInternals<SubagentTimingProjection, unknown>>;
62
+ view: (state: NoInfer<TimingState>) => {
63
+ lastTurnCompleted?: boolean;
64
+ active?: {
65
+ since: number;
66
+ through: number;
67
+ };
68
+ settledMs: number;
69
+ };
70
+ };
71
+ stateVersion: number;
72
+ };
73
+ interface IdentityState {
74
+ /** Identity from the last valid descriptor; absent before one, and after an invalid one. */
75
+ identity?: SubagentIdentityProjection | undefined;
76
+ }
77
+ /**
78
+ * Fold the durable mode/label identity from `subagent/descriptor` events,
79
+ * last-wins: a fork seed may replay an ancestor's descriptor, and the child's
80
+ * own descriptor must override it — the same reset discipline as
81
+ * {@link subagentTimingProjectionDefinition}. A malformed or unknown-version
82
+ * payload resets to the `null` sentinel instead of throwing, so a fork of a
83
+ * healthy ancestor never inherits an identity its own descriptor failed to
84
+ * establish — and the reset survives every JSON push frame, so a consumer
85
+ * holding the earlier identity replaces it instead of keeping it stale;
86
+ * `null` ⟺ no valid descriptor, with the causes deliberately undistinguished.
87
+ */
88
+ export declare const subagentIdentityProjectionDefinition: {
89
+ key: "subagent";
90
+ stateSchema: z.ZodType<IdentityState, unknown, z.core.$ZodTypeInternals<IdentityState, unknown>>;
91
+ init: () => {};
92
+ apply: (state: NoInfer<IdentityState>, event: SessionEvent) => IdentityState;
93
+ wire: {
94
+ viewSchema: z.ZodNullable<z.ZodType<SubagentIdentityProjection, unknown, z.core.$ZodTypeInternals<SubagentIdentityProjection, unknown>>>;
95
+ view: (state: NoInfer<IdentityState>) => SubagentIdentityProjection | null;
96
+ };
97
+ stateVersion: number;
98
+ };
99
+ export {};
100
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Pure session projections for subagent identity (mode/label) and active-turn
3
+ * duration.
4
+ *
5
+ * @module @qlzt/dsh-subagent/projection
6
+ */
7
+ import { z } from 'zod';
8
+ import { SessionSeq } from '@qlzt/dsh-session';
9
+ import { foldSubagentDescriptor } from "./descriptor.js";
10
+ const activeIntervalSchema = z.object({
11
+ since: z.number().int().nonnegative(),
12
+ through: z.number().int().nonnegative(),
13
+ }).strict();
14
+ const projectionSchema = z.object({
15
+ settledMs: z.number().int().nonnegative(),
16
+ active: activeIntervalSchema.optional(),
17
+ lastTurnCompleted: z.boolean().optional(),
18
+ }).strict().transform(({ settledMs, active, lastTurnCompleted }) => ({
19
+ settledMs,
20
+ ...active === undefined ? {} : { active },
21
+ ...lastTurnCompleted === undefined ? {} : { lastTurnCompleted },
22
+ }));
23
+ const timingStateSchema = z.object({
24
+ settledMs: z.number().int().nonnegative(),
25
+ active: activeIntervalSchema.optional(),
26
+ pendingTurnStart: z.number().int().nonnegative().optional(),
27
+ descriptorSeen: z.boolean(),
28
+ lastTurnCompleted: z.boolean().optional(),
29
+ }).strict();
30
+ /**
31
+ * Fold turn boundaries around the child's own durable descriptor.
32
+ *
33
+ * A fork seed may contain an ancestor descriptor and completed turns. Every
34
+ * descriptor therefore resets the accumulated state; the healthy catalog
35
+ * admits only a child with exactly one descriptor in its own suffix, making
36
+ * the final reset the child's authoritative timing origin.
37
+ */
38
+ export const subagentTimingProjectionDefinition = {
39
+ key: 'subagentTiming',
40
+ stateSchema: timingStateSchema,
41
+ init: () => ({ descriptorSeen: false, settledMs: 0 }),
42
+ apply: (state, event) => {
43
+ if (event.type === 'turn/start') {
44
+ const { lastTurnCompleted: _closed, ...openState } = state;
45
+ return state.descriptorSeen
46
+ ? { ...openState, active: { since: event.time, through: event.time } }
47
+ : { ...openState, pendingTurnStart: event.time };
48
+ }
49
+ if (event.type === 'subagent/descriptor') {
50
+ const activeSince = state.active?.since ?? state.pendingTurnStart;
51
+ return {
52
+ descriptorSeen: true,
53
+ settledMs: 0,
54
+ ...(activeSince === undefined
55
+ ? {}
56
+ : { active: { since: activeSince, through: event.time } }),
57
+ };
58
+ }
59
+ if (event.type === 'turn/end') {
60
+ if (!state.descriptorSeen) {
61
+ if (state.pendingTurnStart === undefined)
62
+ return state;
63
+ const { pendingTurnStart: _closed, ...next } = state;
64
+ return next;
65
+ }
66
+ if (state.active === undefined)
67
+ return state;
68
+ const { active, ...rest } = state;
69
+ return {
70
+ ...rest,
71
+ settledMs: state.settledMs + Math.max(0, event.time - active.since),
72
+ lastTurnCompleted: event.data.reason.kind === 'completed',
73
+ };
74
+ }
75
+ if (state.active === undefined)
76
+ return state;
77
+ return { ...state, active: { ...state.active, through: event.time } };
78
+ },
79
+ wire: {
80
+ viewSchema: projectionSchema,
81
+ view: state => ({
82
+ settledMs: state.settledMs,
83
+ ...(state.active === undefined ? {} : { active: state.active }),
84
+ ...(state.lastTurnCompleted === undefined ? {} : { lastTurnCompleted: state.lastTurnCompleted }),
85
+ }),
86
+ },
87
+ stateVersion: 3,
88
+ };
89
+ // The cast bridges only the optional-label arm: Zod's optional output
90
+ // includes explicit `undefined`, which exactOptionalPropertyTypes excludes
91
+ // from the public interface. The no-value state itself is the serializable
92
+ // `null` arm — never `undefined` — so every registry read and push frame
93
+ // survives JSON.stringify losslessly.
94
+ const identityValueSchema = z.discriminatedUnion('mode', [
95
+ z.object({
96
+ mode: z.literal('one-shot'),
97
+ label: z.string().optional(),
98
+ seq: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
99
+ }).strict(),
100
+ z.object({
101
+ mode: z.literal('continuable'),
102
+ label: z.string(),
103
+ seq: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
104
+ }).strict(),
105
+ ]);
106
+ const identitySchema = identityValueSchema.nullable();
107
+ const identityStateSchema = z.object({
108
+ identity: identityValueSchema.optional(),
109
+ }).strict();
110
+ /** Interpret one `subagent/descriptor` event's identity; no value when the payload cannot be trusted. */
111
+ function descriptorIdentity(event) {
112
+ let descriptor;
113
+ try {
114
+ descriptor = foldSubagentDescriptor([event]);
115
+ }
116
+ catch {
117
+ // Only a malformed current-version payload throws in descriptor parsing;
118
+ // a projection fold must never throw, so damage folds to no value.
119
+ descriptor = undefined;
120
+ }
121
+ if (descriptor === undefined)
122
+ return undefined;
123
+ return descriptor.mode === 'one-shot'
124
+ ? {
125
+ mode: 'one-shot',
126
+ ...descriptor.label !== undefined ? { label: descriptor.label } : {},
127
+ seq: event.seq,
128
+ }
129
+ : { mode: 'continuable', label: descriptor.label, seq: event.seq };
130
+ }
131
+ /**
132
+ * Fold the durable mode/label identity from `subagent/descriptor` events,
133
+ * last-wins: a fork seed may replay an ancestor's descriptor, and the child's
134
+ * own descriptor must override it — the same reset discipline as
135
+ * {@link subagentTimingProjectionDefinition}. A malformed or unknown-version
136
+ * payload resets to the `null` sentinel instead of throwing, so a fork of a
137
+ * healthy ancestor never inherits an identity its own descriptor failed to
138
+ * establish — and the reset survives every JSON push frame, so a consumer
139
+ * holding the earlier identity replaces it instead of keeping it stale;
140
+ * `null` ⟺ no valid descriptor, with the causes deliberately undistinguished.
141
+ */
142
+ export const subagentIdentityProjectionDefinition = {
143
+ key: 'subagent',
144
+ stateSchema: identityStateSchema,
145
+ init: () => ({}),
146
+ apply: (state, event) => {
147
+ if (event.type !== 'subagent/descriptor')
148
+ return state;
149
+ const identity = descriptorIdentity(event);
150
+ return identity === undefined ? {} : { identity };
151
+ },
152
+ wire: { viewSchema: identitySchema, view: state => state.identity ?? null },
153
+ // Bumped when the identity gained its `seq` field: an older checkpoint row
154
+ // would replay into a value the schema rejects, so it must refold instead.
155
+ stateVersion: 2,
156
+ };
157
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Child-scoped structured-output tool, prompt instruction, terminal guard, and authoritative
3
+ * result capture for in-process subagents. Each child registers its real schema on its own
4
+ * scope, so concurrent runs do not interact and disposal leaves no global residue. The prompt
5
+ * contribution is ordinary reconstructed request state.
6
+ *
7
+ * Capture commits only after the authoritative `tools/result` succeeds; PTC mode capture also
8
+ * waits for the enclosing `run_code` result. The terminal result marker and monotonic tool
9
+ * guard prevent later calls from reopening a completed structured run.
10
+ * @module @qlzt/dsh-subagent/structured
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ import { type ObjectJsonSchema } from '@qlzt/dsh-tools';
14
+ /** The model-facing tool name a structured child must call to finish. */
15
+ export declare const STRUCTURED_OUTPUT_TOOL = "structured_output";
16
+ /**
17
+ * The instruction registered as the child's trailing scoped prompt section:
18
+ * the demand travels with the
19
+ * tool, as ordinary prompt state of exactly one agent.
20
+ */
21
+ export declare const STRUCTURED_OUTPUT_INSTRUCTION: string;
22
+ /** One structured run's live handle: read the captured value once the child settles. */
23
+ export interface StructuredAttachment {
24
+ /**
25
+ * Input stays closed from capture staging through its final result, including
26
+ * an enclosing PTC execution. A failed result discards its capture and reopens input.
27
+ * @returns whether the activation can accept another message.
28
+ */
29
+ acceptsInput(): boolean;
30
+ /**
31
+ * The captured value, once the child called the tool with valid arguments
32
+ * and the authoritative final tool result accepted that call.
33
+ * @returns the committed value, or undefined while none was accepted.
34
+ */
35
+ captured(): {
36
+ value: unknown;
37
+ } | undefined;
38
+ }
39
+ /**
40
+ * Attach the scoped capture tool, instruction, and enforcement to a child during
41
+ * its creation window. Child disposal removes every registration.
42
+ * @param childCtx - the child agent's scope context (`setup`'s argument).
43
+ * @param schema - the trusted, already-asserted schema subset to enforce (see
44
+ * `assertObjectJsonSchema` in dsh-tools).
45
+ * @param canComplete - whether accepted input and owned child work have finished.
46
+ * @returns the attachment handle (read `captured()` after the child settles).
47
+ */
48
+ export declare function attachStructuredRuntime(childCtx: Context, schema: ObjectJsonSchema, canComplete: () => boolean): StructuredAttachment;
49
+ //# sourceMappingURL=structured.d.ts.map