@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.
- package/dist/index.d.mts +77 -120
- package/dist/index.mjs +43 -49
- package/dist/rows.d.mts +1 -3
- package/dist/rows.mjs +1 -2
- package/package.json +28 -28
- package/src/continuation-messages.ts +8 -3
- package/src/index.ts +5 -4
- package/src/manager.ts +16 -2
- package/src/vendor/dsh-subagent/activation.ts +81 -89
- package/src/vendor/dsh-subagent/archive-admission.ts +38 -45
- package/src/vendor/dsh-subagent/assistant-output.ts +17 -19
- package/src/vendor/dsh-subagent/catalog.ts +79 -111
- package/src/vendor/dsh-subagent/child-agent.ts +83 -92
- package/src/vendor/dsh-subagent/continuation-messages.ts +64 -68
- package/src/vendor/dsh-subagent/control-types.ts +71 -68
- package/src/vendor/dsh-subagent/control.ts +39 -55
- package/src/vendor/dsh-subagent/depth.ts +14 -18
- package/src/vendor/dsh-subagent/descriptor.ts +107 -113
- package/src/vendor/dsh-subagent/error.ts +3 -3
- package/src/vendor/dsh-subagent/index.ts +193 -254
- package/src/vendor/dsh-subagent/internal.ts +17 -21
- package/src/vendor/dsh-subagent/lifecycle.ts +48 -51
- package/src/vendor/dsh-subagent/list-children.ts +57 -65
- package/src/vendor/dsh-subagent/manager.ts +726 -862
- package/src/vendor/dsh-subagent/out-of-process.ts +59 -61
- package/src/vendor/dsh-subagent/projection-types.ts +40 -38
- package/src/vendor/dsh-subagent/projection.ts +85 -107
- package/src/vendor/dsh-subagent/structured.ts +48 -57
- package/src/vendor/dsh-subagent/types.ts +68 -68
|
@@ -10,22 +10,17 @@
|
|
|
10
10
|
* @module @deepseek-ai/dsh-subagent/out-of-process
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { accessSync, constants, statSync } from
|
|
14
|
-
import { isAbsolute } from
|
|
15
|
-
import type { ContentBlock } from
|
|
16
|
-
import type {
|
|
17
|
-
SubagentCapabilities,
|
|
18
|
-
SubagentResult,
|
|
19
|
-
SubagentRun,
|
|
20
|
-
SubagentStopReason,
|
|
21
|
-
} from "./types.ts";
|
|
13
|
+
import { accessSync, constants, statSync } from 'node:fs'
|
|
14
|
+
import { isAbsolute } from 'node:path'
|
|
15
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
16
|
+
import type { SubagentCapabilities, SubagentResult, SubagentRun, SubagentStopReason } from './types.ts'
|
|
22
17
|
|
|
23
18
|
/** Maximum UTF-8 size of {@link SubagentResult.diagnostic}. */
|
|
24
|
-
const MAX_SUBAGENT_DIAGNOSTIC_BYTES = 4_096
|
|
19
|
+
const MAX_SUBAGENT_DIAGNOSTIC_BYTES = 4_096
|
|
25
20
|
|
|
26
|
-
const DIAGNOSTIC_TRUNCATION_SUFFIX =
|
|
27
|
-
const utf8Encoder = new TextEncoder()
|
|
28
|
-
const utf8Decoder = new TextDecoder()
|
|
21
|
+
const DIAGNOSTIC_TRUNCATION_SUFFIX = '\n[diagnostic truncated]'
|
|
22
|
+
const utf8Encoder = new TextEncoder()
|
|
23
|
+
const utf8Decoder = new TextDecoder()
|
|
29
24
|
|
|
30
25
|
/**
|
|
31
26
|
* Limit provider-authored failure detail without splitting a UTF-8 sequence.
|
|
@@ -33,22 +28,23 @@ const utf8Decoder = new TextDecoder();
|
|
|
33
28
|
* @returns the original text, or a visibly truncated value within the limit.
|
|
34
29
|
*/
|
|
35
30
|
function limitSubagentDiagnostic(diagnostic: string): string {
|
|
36
|
-
const bytes = utf8Encoder.encode(diagnostic)
|
|
37
|
-
if (bytes.byteLength <= MAX_SUBAGENT_DIAGNOSTIC_BYTES) return diagnostic
|
|
31
|
+
const bytes = utf8Encoder.encode(diagnostic)
|
|
32
|
+
if (bytes.byteLength <= MAX_SUBAGENT_DIAGNOSTIC_BYTES) return diagnostic
|
|
38
33
|
|
|
39
|
-
const suffixBytes = utf8Encoder.encode(DIAGNOSTIC_TRUNCATION_SUFFIX).byteLength
|
|
40
|
-
let prefixBytes = MAX_SUBAGENT_DIAGNOSTIC_BYTES - suffixBytes
|
|
34
|
+
const suffixBytes = utf8Encoder.encode(DIAGNOSTIC_TRUNCATION_SUFFIX).byteLength
|
|
35
|
+
let prefixBytes = MAX_SUBAGENT_DIAGNOSTIC_BYTES - suffixBytes
|
|
41
36
|
while (((bytes[prefixBytes] as number) & 0b1100_0000) === 0b1000_0000) {
|
|
42
|
-
prefixBytes -= 1
|
|
37
|
+
prefixBytes -= 1
|
|
43
38
|
}
|
|
44
|
-
return utf8Decoder.decode(bytes.subarray(0, prefixBytes))
|
|
39
|
+
return utf8Decoder.decode(bytes.subarray(0, prefixBytes))
|
|
40
|
+
+ DIAGNOSTIC_TRUNCATION_SUFFIX
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
/** Enforce the byte limit on a provider-returned diagnostic. */
|
|
48
44
|
function normalizeSubagentDiagnostic(result: SubagentResult): SubagentResult {
|
|
49
45
|
return result.diagnostic === undefined
|
|
50
46
|
? result
|
|
51
|
-
: { ...result, diagnostic: limitSubagentDiagnostic(result.diagnostic) }
|
|
47
|
+
: { ...result, diagnostic: limitSubagentDiagnostic(result.diagnostic) }
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
/**
|
|
@@ -62,7 +58,7 @@ export const NO_START_CAPABILITIES: SubagentCapabilities = Object.freeze({
|
|
|
62
58
|
agentPreset: false,
|
|
63
59
|
outputSchema: false,
|
|
64
60
|
depthLimit: false,
|
|
65
|
-
})
|
|
61
|
+
})
|
|
66
62
|
|
|
67
63
|
/**
|
|
68
64
|
* Assert a configured timing bound is a positive finite number (it bounds a
|
|
@@ -73,7 +69,7 @@ export const NO_START_CAPABILITIES: SubagentCapabilities = Object.freeze({
|
|
|
73
69
|
*/
|
|
74
70
|
export function assertPositiveFinite(prefix: string, name: string, value: number): void {
|
|
75
71
|
if (!Number.isFinite(value) || value <= 0) {
|
|
76
|
-
throw new Error(`${prefix}: ${name} must be a positive finite number`)
|
|
72
|
+
throw new Error(`${prefix}: ${name} must be a positive finite number`)
|
|
77
73
|
}
|
|
78
74
|
}
|
|
79
75
|
|
|
@@ -84,14 +80,14 @@ export function assertPositiveFinite(prefix: string, name: string, value: number
|
|
|
84
80
|
*/
|
|
85
81
|
function isEnterableDirectory(path: string): boolean {
|
|
86
82
|
try {
|
|
87
|
-
if (!statSync(path).isDirectory()) return false
|
|
88
|
-
accessSync(path, constants.X_OK)
|
|
89
|
-
return true
|
|
83
|
+
if (!statSync(path).isDirectory()) return false
|
|
84
|
+
accessSync(path, constants.X_OK)
|
|
85
|
+
return true
|
|
90
86
|
} catch {
|
|
91
87
|
// statSync/accessSync throw only filesystem access errors here
|
|
92
88
|
// (ENOENT/EACCES/ENOTDIR/…), and every one of them means the path cannot
|
|
93
89
|
// serve as the child's cwd.
|
|
94
|
-
return false
|
|
90
|
+
return false
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
93
|
|
|
@@ -107,12 +103,12 @@ function isEnterableDirectory(path: string): boolean {
|
|
|
107
103
|
*/
|
|
108
104
|
export function assertUsableCwd(prefix: string, label: string, cwd: string): string {
|
|
109
105
|
if (!isAbsolute(cwd)) {
|
|
110
|
-
throw new Error(`${prefix}: ${label} must be an absolute path: ${cwd}`)
|
|
106
|
+
throw new Error(`${prefix}: ${label} must be an absolute path: ${cwd}`)
|
|
111
107
|
}
|
|
112
108
|
if (!isEnterableDirectory(cwd)) {
|
|
113
|
-
throw new Error(`${prefix}: ${label} is not an accessible directory: ${cwd}`)
|
|
109
|
+
throw new Error(`${prefix}: ${label} is not an accessible directory: ${cwd}`)
|
|
114
110
|
}
|
|
115
|
-
return cwd
|
|
111
|
+
return cwd
|
|
116
112
|
}
|
|
117
113
|
|
|
118
114
|
/** Normalize an unknown thrown value to an Error (the catch binding is `unknown`). */
|
|
@@ -121,25 +117,25 @@ function toError(value: unknown): Error {
|
|
|
121
117
|
// `Error`s; the `String(value)` arm is a defensive fallback for a non-Error
|
|
122
118
|
// throw the typed surfaces cannot produce.
|
|
123
119
|
/* v8 ignore next */
|
|
124
|
-
return value instanceof Error ? value : new Error(String(value))
|
|
120
|
+
return value instanceof Error ? value : new Error(String(value))
|
|
125
121
|
}
|
|
126
122
|
|
|
127
123
|
/** Inputs to {@link settleRunResult}. */
|
|
128
124
|
export interface RunResultSettlement {
|
|
129
125
|
/** The turn attempt (typically racing local cancellation); returns the terminal result. */
|
|
130
|
-
attempt: () => Promise<SubagentResult
|
|
126
|
+
attempt: () => Promise<SubagentResult>
|
|
131
127
|
/** Snapshot the provider exposes when cancellation or failure wins settlement. */
|
|
132
|
-
collectOutput: () => readonly ContentBlock[]
|
|
128
|
+
collectOutput: () => readonly ContentBlock[]
|
|
133
129
|
/** Snapshot safe provider-authored detail when a failure wins settlement. */
|
|
134
|
-
collectDiagnostic?: (() => string | undefined) | undefined
|
|
130
|
+
collectDiagnostic?: (() => string | undefined) | undefined
|
|
135
131
|
/** Whether local cancellation settled before the attempt's outcome is observed. */
|
|
136
|
-
cancelled: () => boolean
|
|
132
|
+
cancelled: () => boolean
|
|
137
133
|
/** Diagnostic sink for a failure flattened to a stop reason; a throw from it is contained. */
|
|
138
|
-
onError?: ((error: Error, stopReason: SubagentStopReason) => void) | undefined
|
|
134
|
+
onError?: ((error: Error, stopReason: SubagentStopReason) => void) | undefined
|
|
139
135
|
/** The request's cancellation signal (the listener is removed at settlement). */
|
|
140
|
-
signal: AbortSignal
|
|
136
|
+
signal: AbortSignal
|
|
141
137
|
/** The abort listener registered on {@link signal} at start. */
|
|
142
|
-
onAbort: () => void
|
|
138
|
+
onAbort: () => void
|
|
143
139
|
}
|
|
144
140
|
|
|
145
141
|
/**
|
|
@@ -154,45 +150,47 @@ export interface RunResultSettlement {
|
|
|
154
150
|
*/
|
|
155
151
|
export async function settleRunResult(parts: RunResultSettlement): Promise<SubagentResult> {
|
|
156
152
|
try {
|
|
157
|
-
const result = await parts.attempt()
|
|
153
|
+
const result = await parts.attempt()
|
|
158
154
|
return parts.cancelled()
|
|
159
|
-
? { output: parts.collectOutput(), stopReason:
|
|
160
|
-
: normalizeSubagentDiagnostic(result)
|
|
155
|
+
? { output: parts.collectOutput(), stopReason: 'aborted' }
|
|
156
|
+
: normalizeSubagentDiagnostic(result)
|
|
161
157
|
} catch (error: unknown) {
|
|
162
158
|
// Cover a rejection already queued when cancellation arrives.
|
|
163
|
-
if (parts.cancelled()) return { output: parts.collectOutput(), stopReason:
|
|
159
|
+
if (parts.cancelled()) return { output: parts.collectOutput(), stopReason: 'aborted' }
|
|
164
160
|
// Flatten post-publication transport failures while preserving diagnostics.
|
|
165
161
|
try {
|
|
166
|
-
parts.onError?.(toError(error),
|
|
162
|
+
parts.onError?.(toError(error), 'error')
|
|
167
163
|
} catch {
|
|
168
164
|
// The diagnostic sink cannot reject the run result.
|
|
169
165
|
}
|
|
170
|
-
const collected = parts.collectDiagnostic?.()
|
|
171
|
-
const diagnostic = collected === undefined
|
|
166
|
+
const collected = parts.collectDiagnostic?.()
|
|
167
|
+
const diagnostic = collected === undefined
|
|
168
|
+
? undefined
|
|
169
|
+
: limitSubagentDiagnostic(collected)
|
|
172
170
|
return {
|
|
173
171
|
output: parts.collectOutput(),
|
|
174
|
-
...
|
|
175
|
-
stopReason:
|
|
176
|
-
}
|
|
172
|
+
...diagnostic === undefined ? {} : { diagnostic },
|
|
173
|
+
stopReason: 'error',
|
|
174
|
+
}
|
|
177
175
|
} finally {
|
|
178
|
-
parts.signal.removeEventListener(
|
|
176
|
+
parts.signal.removeEventListener('abort', parts.onAbort)
|
|
179
177
|
}
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
/** Inputs to {@link subprocessRunHandle}. */
|
|
183
181
|
export interface SubprocessRunHandleParts {
|
|
184
182
|
/** The parent-scoped run id. */
|
|
185
|
-
id: SubagentRun[
|
|
183
|
+
id: SubagentRun['id']
|
|
186
184
|
/** The flattened, never-rejecting result (the seam contract). */
|
|
187
|
-
result: Promise<SubagentResult
|
|
185
|
+
result: Promise<SubagentResult>
|
|
188
186
|
/** The request's cancellation signal (the listener is removed on dispose). */
|
|
189
|
-
signal: AbortSignal
|
|
187
|
+
signal: AbortSignal
|
|
190
188
|
/** The abort listener registered on {@link signal} at start. */
|
|
191
|
-
onAbort: () => void
|
|
189
|
+
onAbort: () => void
|
|
192
190
|
/** Settle local cancellation so {@link result} resolves without the child. */
|
|
193
|
-
requestCancel: () => void
|
|
191
|
+
requestCancel: () => void
|
|
194
192
|
/** Tear the child process down to quiescence (backend-owned ladder). */
|
|
195
|
-
teardown: () => Promise<void
|
|
193
|
+
teardown: () => Promise<void>
|
|
196
194
|
}
|
|
197
195
|
|
|
198
196
|
/**
|
|
@@ -204,16 +202,16 @@ export interface SubprocessRunHandleParts {
|
|
|
204
202
|
* @returns the external run handle.
|
|
205
203
|
*/
|
|
206
204
|
export function subprocessRunHandle(parts: SubprocessRunHandleParts): SubagentRun {
|
|
207
|
-
let disposal: Promise<void> | undefined
|
|
205
|
+
let disposal: Promise<void> | undefined
|
|
208
206
|
return {
|
|
209
207
|
id: parts.id,
|
|
210
208
|
result: parts.result,
|
|
211
209
|
dispose(): Promise<void> {
|
|
212
|
-
if (disposal !== undefined) return disposal
|
|
213
|
-
parts.signal.removeEventListener(
|
|
214
|
-
parts.requestCancel()
|
|
215
|
-
disposal = parts.teardown()
|
|
216
|
-
return disposal
|
|
210
|
+
if (disposal !== undefined) return disposal
|
|
211
|
+
parts.signal.removeEventListener('abort', parts.onAbort)
|
|
212
|
+
parts.requestCancel()
|
|
213
|
+
disposal = parts.teardown()
|
|
214
|
+
return disposal
|
|
217
215
|
},
|
|
218
|
-
}
|
|
216
|
+
}
|
|
219
217
|
}
|
|
@@ -4,35 +4,37 @@
|
|
|
4
4
|
* @module @deepseek-ai/dsh-subagent/projection-types
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { SessionId, SessionSeq } from
|
|
7
|
+
import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types'
|
|
8
8
|
|
|
9
9
|
/** One current direct-child discovery row materialized from parent facts. */
|
|
10
|
-
export type SubagentCatalogEntry =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
export type SubagentCatalogEntry =
|
|
11
|
+
& {
|
|
12
|
+
readonly id: SessionId
|
|
13
|
+
readonly createdAt: number
|
|
14
|
+
}
|
|
15
|
+
& (
|
|
16
|
+
| { readonly mode: 'one-shot'; readonly label?: string }
|
|
17
|
+
| { readonly mode: 'external'; readonly label?: string }
|
|
18
|
+
| { readonly mode: 'continuable'; readonly label: string }
|
|
19
|
+
| { readonly mode: 'unknown'; readonly label?: string }
|
|
20
|
+
)
|
|
19
21
|
|
|
20
22
|
/** Durable active-turn timing for one descriptor-backed child session. */
|
|
21
23
|
export interface SubagentTimingProjection {
|
|
22
24
|
/** Milliseconds accumulated across completed turns after the child's own descriptor. */
|
|
23
|
-
settledMs: number
|
|
25
|
+
settledMs: number
|
|
24
26
|
/** Same-cut bounds of the currently open turn, when one has not reached `turn/end`. */
|
|
25
27
|
active?: {
|
|
26
28
|
/** Start of the open turn. */
|
|
27
|
-
since: number
|
|
29
|
+
since: number
|
|
28
30
|
/** Latest event time folded into this projection cut. */
|
|
29
|
-
through: number
|
|
30
|
-
}
|
|
31
|
+
through: number
|
|
32
|
+
}
|
|
31
33
|
/**
|
|
32
34
|
* Whether the latest closed turn after the child's own descriptor completed
|
|
33
35
|
* normally; absent while a turn is open or before one closes.
|
|
34
36
|
*/
|
|
35
|
-
lastTurnCompleted?: boolean
|
|
37
|
+
lastTurnCompleted?: boolean
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -43,33 +45,33 @@ export interface SubagentTimingProjection {
|
|
|
43
45
|
*/
|
|
44
46
|
export type SubagentIdentityProjection =
|
|
45
47
|
| {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|
|
58
60
|
| {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
/** A resumable conversation. */
|
|
62
|
+
mode: 'continuable'
|
|
63
|
+
/** Durable creation label from the child's descriptor. */
|
|
64
|
+
label: string
|
|
65
|
+
/** Seq of the folded descriptor event; see the one-shot arm for the own-suffix proof. */
|
|
66
|
+
seq: SessionSeq
|
|
67
|
+
}
|
|
66
68
|
|
|
67
|
-
declare module
|
|
69
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
68
70
|
interface SessionProjectionMap {
|
|
69
71
|
/** Direct children in parent catalog event order, excluding fork-inherited facts. */
|
|
70
|
-
subagentCatalog: SubagentCatalogEntry[]
|
|
72
|
+
subagentCatalog: SubagentCatalogEntry[]
|
|
71
73
|
/** Active-turn duration and latest closed-turn completion for a descriptor-backed subagent session. */
|
|
72
|
-
subagentTiming: SubagentTimingProjection
|
|
74
|
+
subagentTiming: SubagentTimingProjection
|
|
73
75
|
/**
|
|
74
76
|
* Identity of a descriptor-backed subagent session. `null` ⟺ no valid
|
|
75
77
|
* descriptor (missing, malformed, or unrecognized-version — deliberately
|
|
@@ -79,6 +81,6 @@ declare module "@deepseek-ai/dsh-session-projection/types" {
|
|
|
79
81
|
* identity would survive on the receiving side. The entry itself stays
|
|
80
82
|
* non-optional.
|
|
81
83
|
*/
|
|
82
|
-
subagent: SubagentIdentityProjection | null
|
|
84
|
+
subagent: SubagentIdentityProjection | null
|
|
83
85
|
}
|
|
84
86
|
}
|
|
@@ -5,64 +5,50 @@
|
|
|
5
5
|
* @module @deepseek-ai/dsh-subagent/projection
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { z } from
|
|
9
|
-
import { SessionSeq } from
|
|
10
|
-
import type { ProjectionDefinition } from
|
|
11
|
-
import type { SessionEvent } from
|
|
12
|
-
import { foldSubagentDescriptor } from
|
|
13
|
-
import type { SubagentDescriptorData } from
|
|
14
|
-
import type { SubagentIdentityProjection, SubagentTimingProjection } from
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { SessionSeq } from '@deepseek-ai/dsh-session'
|
|
10
|
+
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
|
|
11
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
12
|
+
import { foldSubagentDescriptor } from './descriptor.ts'
|
|
13
|
+
import type { SubagentDescriptorData } from './descriptor.ts'
|
|
14
|
+
import type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts'
|
|
15
15
|
|
|
16
16
|
/** Fold state for a subagent's latest timing snapshot. */
|
|
17
17
|
export interface TimingState {
|
|
18
18
|
/** Milliseconds accumulated across completed post-descriptor turns. */
|
|
19
|
-
settledMs: number
|
|
19
|
+
settledMs: number
|
|
20
20
|
/** Current open interval kept paired inside the fold. */
|
|
21
|
-
active?: { since: number; through: number } | undefined
|
|
21
|
+
active?: { since: number; through: number } | undefined
|
|
22
22
|
/** Latest pre-descriptor turn start, promoted when the child's own descriptor arrives. */
|
|
23
|
-
pendingTurnStart?: number | undefined
|
|
23
|
+
pendingTurnStart?: number | undefined
|
|
24
24
|
/** Whether the fold has crossed a descriptor in this logical log. */
|
|
25
|
-
descriptorSeen: boolean
|
|
25
|
+
descriptorSeen: boolean
|
|
26
26
|
/** Whether the latest closed post-descriptor turn completed normally; absent while a turn is open or before one closes. */
|
|
27
|
-
lastTurnCompleted?: boolean | undefined
|
|
27
|
+
lastTurnCompleted?: boolean | undefined
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const activeIntervalSchema = z
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
})
|
|
35
|
-
.strict();
|
|
30
|
+
const activeIntervalSchema = z.object({
|
|
31
|
+
since: z.number().int().nonnegative(),
|
|
32
|
+
through: z.number().int().nonnegative(),
|
|
33
|
+
}).strict()
|
|
36
34
|
|
|
37
|
-
const projectionSchema: z.ZodType<SubagentTimingProjection> = z
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
...(active === undefined ? {} : { active }),
|
|
47
|
-
...(lastTurnCompleted === undefined ? {} : { lastTurnCompleted }),
|
|
48
|
-
}));
|
|
35
|
+
const projectionSchema: z.ZodType<SubagentTimingProjection> = z.object({
|
|
36
|
+
settledMs: z.number().int().nonnegative(),
|
|
37
|
+
active: activeIntervalSchema.optional(),
|
|
38
|
+
lastTurnCompleted: z.boolean().optional(),
|
|
39
|
+
}).strict().transform(({ settledMs, active, lastTurnCompleted }) => ({
|
|
40
|
+
settledMs,
|
|
41
|
+
...active === undefined ? {} : { active },
|
|
42
|
+
...lastTurnCompleted === undefined ? {} : { lastTurnCompleted },
|
|
43
|
+
}))
|
|
49
44
|
|
|
50
|
-
const timingStateSchema: z.ZodType<TimingState> = z
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
58
|
-
.strict();
|
|
59
|
-
|
|
60
|
-
declare module "@deepseek-ai/dsh-session-projection/types" {
|
|
61
|
-
interface SessionProjectionStateMap {
|
|
62
|
-
subagentTiming: TimingState;
|
|
63
|
-
subagent: IdentityState;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
45
|
+
const timingStateSchema: z.ZodType<TimingState> = z.object({
|
|
46
|
+
settledMs: z.number().int().nonnegative(),
|
|
47
|
+
active: activeIntervalSchema.optional(),
|
|
48
|
+
pendingTurnStart: z.number().int().nonnegative().optional(),
|
|
49
|
+
descriptorSeen: z.boolean(),
|
|
50
|
+
lastTurnCompleted: z.boolean().optional(),
|
|
51
|
+
}).strict()
|
|
66
52
|
|
|
67
53
|
/**
|
|
68
54
|
* Fold turn boundaries around the child's own durable descriptor.
|
|
@@ -73,59 +59,57 @@ declare module "@deepseek-ai/dsh-session-projection/types" {
|
|
|
73
59
|
* the final reset the child's authoritative timing origin.
|
|
74
60
|
*/
|
|
75
61
|
export const subagentTimingProjectionDefinition = {
|
|
76
|
-
key:
|
|
62
|
+
key: 'subagentTiming',
|
|
77
63
|
stateSchema: timingStateSchema,
|
|
78
64
|
init: () => ({ descriptorSeen: false, settledMs: 0 }),
|
|
79
65
|
apply: (state, event) => {
|
|
80
|
-
if (event.type ===
|
|
81
|
-
const { lastTurnCompleted: _closed, ...openState } = state
|
|
66
|
+
if (event.type === 'turn/start') {
|
|
67
|
+
const { lastTurnCompleted: _closed, ...openState } = state
|
|
82
68
|
return state.descriptorSeen
|
|
83
69
|
? { ...openState, active: { since: event.time, through: event.time } }
|
|
84
|
-
: { ...openState, pendingTurnStart: event.time }
|
|
70
|
+
: { ...openState, pendingTurnStart: event.time }
|
|
85
71
|
}
|
|
86
|
-
if (event.type ===
|
|
87
|
-
const activeSince = state.active?.since ?? state.pendingTurnStart
|
|
72
|
+
if (event.type === 'subagent/descriptor') {
|
|
73
|
+
const activeSince = state.active?.since ?? state.pendingTurnStart
|
|
88
74
|
return {
|
|
89
75
|
descriptorSeen: true,
|
|
90
76
|
settledMs: 0,
|
|
91
77
|
...(activeSince === undefined
|
|
92
78
|
? {}
|
|
93
79
|
: { active: { since: activeSince, through: event.time } }),
|
|
94
|
-
}
|
|
80
|
+
}
|
|
95
81
|
}
|
|
96
|
-
if (event.type ===
|
|
82
|
+
if (event.type === 'turn/end') {
|
|
97
83
|
if (!state.descriptorSeen) {
|
|
98
|
-
if (state.pendingTurnStart === undefined) return state
|
|
99
|
-
const { pendingTurnStart: _closed, ...next } = state
|
|
100
|
-
return next
|
|
84
|
+
if (state.pendingTurnStart === undefined) return state
|
|
85
|
+
const { pendingTurnStart: _closed, ...next } = state
|
|
86
|
+
return next
|
|
101
87
|
}
|
|
102
|
-
if (state.active === undefined) return state
|
|
103
|
-
const { active, ...rest } = state
|
|
88
|
+
if (state.active === undefined) return state
|
|
89
|
+
const { active, ...rest } = state
|
|
104
90
|
return {
|
|
105
91
|
...rest,
|
|
106
92
|
settledMs: state.settledMs + Math.max(0, event.time - active.since),
|
|
107
|
-
lastTurnCompleted: event.data.reason.kind ===
|
|
108
|
-
}
|
|
93
|
+
lastTurnCompleted: event.data.reason.kind === 'completed',
|
|
94
|
+
}
|
|
109
95
|
}
|
|
110
|
-
if (state.active === undefined) return state
|
|
111
|
-
return { ...state, active: { ...state.active, through: event.time } }
|
|
96
|
+
if (state.active === undefined) return state
|
|
97
|
+
return { ...state, active: { ...state.active, through: event.time } }
|
|
112
98
|
},
|
|
113
99
|
wire: {
|
|
114
100
|
viewSchema: projectionSchema,
|
|
115
|
-
view:
|
|
101
|
+
view: state => ({
|
|
116
102
|
settledMs: state.settledMs,
|
|
117
103
|
...(state.active === undefined ? {} : { active: state.active }),
|
|
118
|
-
...(state.lastTurnCompleted === undefined
|
|
119
|
-
? {}
|
|
120
|
-
: { lastTurnCompleted: state.lastTurnCompleted }),
|
|
104
|
+
...(state.lastTurnCompleted === undefined ? {} : { lastTurnCompleted: state.lastTurnCompleted }),
|
|
121
105
|
}),
|
|
122
106
|
},
|
|
123
107
|
stateVersion: 3,
|
|
124
|
-
} satisfies ProjectionDefinition<
|
|
108
|
+
} satisfies ProjectionDefinition<'subagentTiming', TimingState>
|
|
125
109
|
|
|
126
110
|
interface IdentityState {
|
|
127
111
|
/** Identity from the last valid descriptor; absent before one, and after an invalid one. */
|
|
128
|
-
identity?: SubagentIdentityProjection | undefined
|
|
112
|
+
identity?: SubagentIdentityProjection | undefined
|
|
129
113
|
}
|
|
130
114
|
|
|
131
115
|
// The cast bridges only the optional-label arm: Zod's optional output
|
|
@@ -133,49 +117,43 @@ interface IdentityState {
|
|
|
133
117
|
// from the public interface. The no-value state itself is the serializable
|
|
134
118
|
// `null` arm — never `undefined` — so every registry read and push frame
|
|
135
119
|
// survives JSON.stringify losslessly.
|
|
136
|
-
const identityValueSchema = z.discriminatedUnion(
|
|
137
|
-
z
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
seq: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
149
|
-
})
|
|
150
|
-
.strict(),
|
|
151
|
-
]) as unknown as z.ZodType<SubagentIdentityProjection>;
|
|
120
|
+
const identityValueSchema = z.discriminatedUnion('mode', [
|
|
121
|
+
z.object({
|
|
122
|
+
mode: z.literal('one-shot'),
|
|
123
|
+
label: z.string().optional(),
|
|
124
|
+
seq: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
125
|
+
}).strict(),
|
|
126
|
+
z.object({
|
|
127
|
+
mode: z.literal('continuable'),
|
|
128
|
+
label: z.string(),
|
|
129
|
+
seq: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
130
|
+
}).strict(),
|
|
131
|
+
]) as unknown as z.ZodType<SubagentIdentityProjection>
|
|
152
132
|
|
|
153
|
-
const identitySchema = identityValueSchema.nullable()
|
|
133
|
+
const identitySchema = identityValueSchema.nullable()
|
|
154
134
|
|
|
155
|
-
const identityStateSchema: z.ZodType<IdentityState> = z
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
})
|
|
159
|
-
.strict();
|
|
135
|
+
const identityStateSchema: z.ZodType<IdentityState> = z.object({
|
|
136
|
+
identity: identityValueSchema.optional(),
|
|
137
|
+
}).strict()
|
|
160
138
|
|
|
161
139
|
/** Interpret one `subagent/descriptor` event's identity; no value when the payload cannot be trusted. */
|
|
162
140
|
function descriptorIdentity(event: SessionEvent): SubagentIdentityProjection | undefined {
|
|
163
|
-
let descriptor: SubagentDescriptorData | undefined
|
|
141
|
+
let descriptor: SubagentDescriptorData | undefined
|
|
164
142
|
try {
|
|
165
|
-
descriptor = foldSubagentDescriptor([event])
|
|
143
|
+
descriptor = foldSubagentDescriptor([event])
|
|
166
144
|
} catch {
|
|
167
145
|
// Only a malformed current-version payload throws in descriptor parsing;
|
|
168
146
|
// a projection fold must never throw, so damage folds to no value.
|
|
169
|
-
descriptor = undefined
|
|
147
|
+
descriptor = undefined
|
|
170
148
|
}
|
|
171
|
-
if (descriptor === undefined) return undefined
|
|
172
|
-
return descriptor.mode ===
|
|
149
|
+
if (descriptor === undefined) return undefined
|
|
150
|
+
return descriptor.mode === 'one-shot'
|
|
173
151
|
? {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
: { mode:
|
|
152
|
+
mode: 'one-shot',
|
|
153
|
+
...descriptor.label !== undefined ? { label: descriptor.label } : {},
|
|
154
|
+
seq: event.seq,
|
|
155
|
+
}
|
|
156
|
+
: { mode: 'continuable', label: descriptor.label, seq: event.seq }
|
|
179
157
|
}
|
|
180
158
|
|
|
181
159
|
/**
|
|
@@ -190,16 +168,16 @@ function descriptorIdentity(event: SessionEvent): SubagentIdentityProjection | u
|
|
|
190
168
|
* `null` ⟺ no valid descriptor, with the causes deliberately undistinguished.
|
|
191
169
|
*/
|
|
192
170
|
export const subagentIdentityProjectionDefinition = {
|
|
193
|
-
key:
|
|
171
|
+
key: 'subagent',
|
|
194
172
|
stateSchema: identityStateSchema,
|
|
195
173
|
init: () => ({}),
|
|
196
174
|
apply: (state, event) => {
|
|
197
|
-
if (event.type !==
|
|
198
|
-
const identity = descriptorIdentity(event)
|
|
199
|
-
return identity === undefined ? {} : { identity }
|
|
175
|
+
if (event.type !== 'subagent/descriptor') return state
|
|
176
|
+
const identity = descriptorIdentity(event)
|
|
177
|
+
return identity === undefined ? {} : { identity }
|
|
200
178
|
},
|
|
201
|
-
wire: { viewSchema: identitySchema, view:
|
|
179
|
+
wire: { viewSchema: identitySchema, view: state => state.identity ?? null },
|
|
202
180
|
// Bumped when the identity gained its `seq` field: an older checkpoint row
|
|
203
181
|
// would replay into a value the schema rejects, so it must refold instead.
|
|
204
182
|
stateVersion: 2,
|
|
205
|
-
} satisfies ProjectionDefinition<
|
|
183
|
+
} satisfies ProjectionDefinition<'subagent', IdentityState>
|