@deepseek-ai/dsh-session 0.2.0-rc.1 → 0.2.1-alpha.1
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.i18n.yaml +8 -8
- package/README.md +3 -4
- package/README.zh.md +3 -4
- package/lib/index.js +2 -2
- package/lib/types/index.d.ts +2 -6
- package/lib/types/index.js +2 -2
- package/lib/types/surface.js +2 -8
- package/lib/types/types.d.ts +2 -2
- package/package.json +10 -16
- package/lib/invariant.js +0 -174
- package/lib/types/invariant.d.ts +0 -18
- package/lib/types/invariant.js +0 -207
package/README.i18n.yaml
CHANGED
|
@@ -42,17 +42,17 @@
|
|
|
42
42
|
en: bbfb53c50100ab13
|
|
43
43
|
zh: 303a8ee47f4f656a
|
|
44
44
|
/deepseek-ai-dsh-session/understand-the-implementation/source-map:
|
|
45
|
-
en:
|
|
46
|
-
zh:
|
|
45
|
+
en: c596a8f74b4f1b79
|
|
46
|
+
zh: a29f23c4c1d1ff75
|
|
47
47
|
/deepseek-ai-dsh-session/understand-the-implementation/append-validation:
|
|
48
48
|
en: 5c09583bc8e1ffc6
|
|
49
49
|
zh: 3b25552de2d1f9b6
|
|
50
50
|
/deepseek-ai-dsh-session/understand-the-implementation/shared-recovery:
|
|
51
|
-
en:
|
|
52
|
-
zh:
|
|
51
|
+
en: 160b35afd9d866e4
|
|
52
|
+
zh: cd1021e0ef567f87
|
|
53
53
|
/deepseek-ai-dsh-session/understand-the-implementation/derived-history:
|
|
54
|
-
en:
|
|
55
|
-
zh:
|
|
54
|
+
en: 4a9861492a8c3869
|
|
55
|
+
zh: e9566de647206230
|
|
56
56
|
/deepseek-ai-dsh-session/understand-the-implementation/the-request-header:
|
|
57
57
|
en: 1349f47bea4dab00
|
|
58
58
|
zh: 20cc03f2c87ec492
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
en: 9c2efc3e9064e3c4
|
|
79
79
|
zh: 9a7783991b322750
|
|
80
80
|
/deepseek-ai-dsh-session/model-experience/tool-result-recovery-and-fork-results/what-the-model-sees:
|
|
81
|
-
en:
|
|
82
|
-
zh:
|
|
81
|
+
en: d15c350efc66e832
|
|
82
|
+
zh: b769ba23dbb374e7
|
|
83
83
|
/deepseek-ai-dsh-session/model-experience/tool-result-recovery-and-fork-results/token-effect:
|
|
84
84
|
en: 1339facbd3d7cee0
|
|
85
85
|
zh: 9d392a053ef90032
|
package/README.md
CHANGED
|
@@ -101,7 +101,6 @@ The package is built on event sourcing: a `Session` is an append-only log of typ
|
|
|
101
101
|
| [`src/request-header.ts`](src/request-header.ts) | `request/header` folding and reconstruction |
|
|
102
102
|
| [`dsh-util-values`](../../util/values/README.md) | Shared lossless JSON validation and detached snapshots |
|
|
103
103
|
| [`src/repair.ts`](src/repair.ts) | Shared tool-result recovery for failed steps, interrupted logs, and fork seeds |
|
|
104
|
-
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion: seq, turn/step enclosure, tool call/result pairing |
|
|
105
104
|
|
|
106
105
|
### Append validation
|
|
107
106
|
|
|
@@ -109,11 +108,11 @@ Every append uses the shared iterative `snapshotJsonValue()` pass, which reads,
|
|
|
109
108
|
|
|
110
109
|
### Shared recovery
|
|
111
110
|
|
|
112
|
-
`ToolCallRecovery` tracks unanswered requests from committed events without retaining event history. AgentLoop observes live steps; crash recovery and fork-seed construction replay their prefixes through `openTurnClosers`. Live failures and crash recovery use interrupted-result wording by default; fork construction passes the fork cause to select its distinct retry guidance. The caller appends recovery results before closing the step ([
|
|
111
|
+
`ToolCallRecovery` tracks unanswered requests from committed events without retaining event history. AgentLoop observes live steps; crash recovery and fork-seed construction replay their prefixes through `openTurnClosers`. Live failures and crash recovery use interrupted-result wording by default; fork construction passes the fork cause to select its distinct retry guidance. The caller appends recovery results before closing the step ([reference](../agent-loop/README.md)).
|
|
113
112
|
|
|
114
113
|
### Derived history
|
|
115
114
|
|
|
116
|
-
`deriveMessages()` caches deep-frozen projections and returns a fresh array per call. The surface event types (`system/message`, `developer/message`, `user/message`, `assistant/message`, `tool/result`) supply their recorded message identities and content; empty-content system and developer nodes project to no message. Plugin-owned projections change derived content without mutating recorded messages. Replacements and projection decisions invalidate the cache. Embedded Assistant streams and `assistant/attempt` events remain replay and diagnostic data only.
|
|
115
|
+
`deriveMessages()` caches deep-frozen projections and returns a fresh array per call. The surface event types (`system/message`, `developer/message`, `user/message`, `assistant/message`, `tool/result`) supply their recorded message identities and content; empty-content system and developer nodes project to no message. Plugin-owned projections change derived content without mutating recorded messages. Replacements and projection decisions invalidate the cache. Embedded Assistant streams and `assistant/attempt` events remain replay and diagnostic data only. Ordinary prompts and injected context project without automatic type- or source-specific wrappers. Producers own any framing in their recorded content.
|
|
117
116
|
|
|
118
117
|
### The request header
|
|
119
118
|
|
|
@@ -157,7 +156,7 @@ Appended surface entries preserve reusable prefixes. A `replace` operation inval
|
|
|
157
156
|
|
|
158
157
|
#### What the model sees
|
|
159
158
|
|
|
160
|
-
If recovery finds an assistant tool request with no durable `tool/call`, its synthetic `TOOL_NOT_STARTED` result says `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.` If a durable `tool/call` has no result, its `TOOL_OUTCOME_UNKNOWN` result says `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.` Fork-generated results describe only the inherited records: the parent may have started or completed a call after the selected event. `TOOL_NOT_STARTED` means the prefix contains no start record; `TOOL_OUTCOME_UNKNOWN` means it contains a start but no result. Both tell the model to retry only read-only or idempotent operations without further checks; operations with side effects require external verification or user input. See the [fork
|
|
159
|
+
If recovery finds an assistant tool request with no durable `tool/call`, its synthetic `TOOL_NOT_STARTED` result says `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.` If a durable `tool/call` has no result, its `TOOL_OUTCOME_UNKNOWN` result says `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.` Fork-generated results describe only the inherited records: the parent may have started or completed a call after the selected event. `TOOL_NOT_STARTED` means the prefix contains no start record; `TOOL_OUTCOME_UNKNOWN` means it contains a start but no result. Both tell the model to retry only read-only or idempotent operations without further checks; operations with side effects require external verification or user input. See the [fork reference](src/fork.ts).
|
|
161
160
|
|
|
162
161
|
#### Token effect
|
|
163
162
|
|
package/README.zh.md
CHANGED
|
@@ -101,7 +101,6 @@ session.deriveMessages() // the derived model history
|
|
|
101
101
|
| [`src/request-header.ts`](src/request-header.ts) | `request/header` 折叠与重建 |
|
|
102
102
|
| [`dsh-util-values`](../../util/values/README.zh.md) | 共享无损 JSON 校验与分离式快照 |
|
|
103
103
|
| [`src/repair.ts`](src/repair.ts) | 失败步骤、中断日志与 fork 种子共享的工具结果恢复 |
|
|
104
|
-
| [`src/invariant.ts`](src/invariant.ts) | 不变式配套:序号、轮次/步骤闭合、工具调用/结果配对 |
|
|
105
104
|
|
|
106
105
|
### 追加校验
|
|
107
106
|
|
|
@@ -109,11 +108,11 @@ session.deriveMessages() // the derived model history
|
|
|
109
108
|
|
|
110
109
|
### 共享恢复逻辑
|
|
111
110
|
|
|
112
|
-
`ToolCallRecovery` 从已提交事件中跟踪尚无结果的请求,不保留事件历史。AgentLoop 观察实时步骤;崩溃恢复与 fork 种子构造通过 `openTurnClosers` 回放各自的前缀。实时失败与崩溃恢复默认使用中断结果文案;fork 构造传入 fork 原因,以选择其专用的重试指引。调用方在关闭步骤之前追加恢复结果([
|
|
111
|
+
`ToolCallRecovery` 从已提交事件中跟踪尚无结果的请求,不保留事件历史。AgentLoop 观察实时步骤;崩溃恢复与 fork 种子构造通过 `openTurnClosers` 回放各自的前缀。实时失败与崩溃恢复默认使用中断结果文案;fork 构造传入 fork 原因,以选择其专用的重试指引。调用方在关闭步骤之前追加恢复结果([参考](../agent-loop/README.zh.md))。
|
|
113
112
|
|
|
114
113
|
### 派生历史
|
|
115
114
|
|
|
116
|
-
`deriveMessages()` 缓存深度冻结的派生消息,每次调用返回新数组。surface 事件类型(`system/message`、`developer/message`、`user/message`、`assistant/message`、`tool/result`)提供记录的消息身份和内容,空内容的 system 和 developer 节点不派生消息。插件拥有的投影修改派生内容,不修改记录的消息。替换和投影决策使缓存失效。嵌入式 Assistant stream 与 `assistant/attempt`
|
|
115
|
+
`deriveMessages()` 缓存深度冻结的派生消息,每次调用返回新数组。surface 事件类型(`system/message`、`developer/message`、`user/message`、`assistant/message`、`tool/result`)提供记录的消息身份和内容,空内容的 system 和 developer 节点不派生消息。插件拥有的投影修改派生内容,不修改记录的消息。替换和投影决策使缓存失效。嵌入式 Assistant stream 与 `assistant/attempt` 事件只保留回放和诊断数据。普通提示与注入上下文在派生时不会按类型或来源自动添加包装。生产方负责记录内容中的所有框定格式。
|
|
117
116
|
|
|
118
117
|
### 请求头
|
|
119
118
|
|
|
@@ -157,7 +156,7 @@ session.deriveMessages() // the derived model history
|
|
|
157
156
|
|
|
158
157
|
#### 模型看到什么
|
|
159
158
|
|
|
160
|
-
如果恢复发现 assistant 工具请求没有持久 `tool/call`,其合成 `TOOL_NOT_STARTED` 结果内容为 `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.`。如果持久 `tool/call` 没有结果,其 `TOOL_OUTCOME_UNKNOWN` 结果内容为 `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.`。 Fork 生成的结果只描述继承记录:父会话可能已经在所选事件之后启动或完成调用。`TOOL_NOT_STARTED` 表示前缀中没有启动记录;`TOOL_OUTCOME_UNKNOWN` 表示有启动记录但没有结果。两者都要求模型仅对只读或幂等操作直接重试;有副作用的操作需要先验证外部状态或询问用户。参见 [fork
|
|
159
|
+
如果恢复发现 assistant 工具请求没有持久 `tool/call`,其合成 `TOOL_NOT_STARTED` 结果内容为 `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.`。如果持久 `tool/call` 没有结果,其 `TOOL_OUTCOME_UNKNOWN` 结果内容为 `The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.`。 Fork 生成的结果只描述继承记录:父会话可能已经在所选事件之后启动或完成调用。`TOOL_NOT_STARTED` 表示前缀中没有启动记录;`TOOL_OUTCOME_UNKNOWN` 表示有启动记录但没有结果。两者都要求模型仅对只读或幂等操作直接重试;有副作用的操作需要先验证外部状态或询问用户。参见 [fork 参考](src/fork.ts)。
|
|
161
160
|
|
|
162
161
|
#### Token 影响
|
|
163
162
|
|
package/lib/index.js
CHANGED
|
@@ -1822,8 +1822,8 @@ var SessionStore = class extends Service {
|
|
|
1822
1822
|
* store owns the carrier, so callers (the checkpoint policy's per-request
|
|
1823
1823
|
* barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
|
|
1824
1824
|
* that flush themselves before reading storage) must come through here
|
|
1825
|
-
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
1826
|
-
* one spelling
|
|
1825
|
+
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
1826
|
+
* and one spelling.
|
|
1827
1827
|
* @param session - the session whose buffered events must reach durable storage.
|
|
1828
1828
|
* @returns whether at least one durability listener participated, after every
|
|
1829
1829
|
* listener has settled successfully.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -36,7 +36,6 @@ declare module '@deepseek-ai/cordis' {
|
|
|
36
36
|
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
|
|
37
37
|
* receive only sessions entered through that agent's context.
|
|
38
38
|
* @param session - the session just entered and announced.
|
|
39
|
-
* @dshScopeScan unsupported
|
|
40
39
|
* @mode emit
|
|
41
40
|
*/
|
|
42
41
|
'session/created'(this: Scoped<Session>, session: Session): void;
|
|
@@ -46,7 +45,6 @@ declare module '@deepseek-ai/cordis' {
|
|
|
46
45
|
* did not begin. Listener failures are logged and contained.
|
|
47
46
|
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
|
|
48
47
|
* @param session - the session that is no longer live in the store.
|
|
49
|
-
* @dshScopeScan unsupported
|
|
50
48
|
* @mode emit
|
|
51
49
|
*/
|
|
52
50
|
'session/disposed'(this: Scoped<Session>, session: Session): void;
|
|
@@ -58,7 +56,6 @@ declare module '@deepseek-ai/cordis' {
|
|
|
58
56
|
* receive only events from sessions entered through that agent's context.
|
|
59
57
|
* @param session - the session whose log grew.
|
|
60
58
|
* @param event - the appended event, exactly as recorded.
|
|
61
|
-
* @dshScopeScan unsupported
|
|
62
59
|
* @mode emit
|
|
63
60
|
*/
|
|
64
61
|
'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void;
|
|
@@ -67,7 +64,6 @@ declare module '@deepseek-ai/cordis' {
|
|
|
67
64
|
* caller awaits all of them, with no waterfall veto. Scope-filtered dispatch
|
|
68
65
|
* (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
|
|
69
66
|
* @param session - the session whose buffered events must reach durable storage.
|
|
70
|
-
* @dshScopeScan unsupported
|
|
71
67
|
* @mode parallel
|
|
72
68
|
*/
|
|
73
69
|
'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void;
|
|
@@ -429,8 +425,8 @@ export declare class SessionStore extends Service {
|
|
|
429
425
|
* store owns the carrier, so callers (the checkpoint policy's per-request
|
|
430
426
|
* barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
|
|
431
427
|
* that flush themselves before reading storage) must come through here
|
|
432
|
-
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
433
|
-
* one spelling
|
|
428
|
+
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
429
|
+
* and one spelling.
|
|
434
430
|
* @param session - the session whose buffered events must reach durable storage.
|
|
435
431
|
* @returns whether at least one durability listener participated, after every
|
|
436
432
|
* listener has settled successfully.
|
package/lib/types/index.js
CHANGED
|
@@ -999,8 +999,8 @@ export class SessionStore extends Service {
|
|
|
999
999
|
* store owns the carrier, so callers (the checkpoint policy's per-request
|
|
1000
1000
|
* barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
|
|
1001
1001
|
* that flush themselves before reading storage) must come through here
|
|
1002
|
-
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
1003
|
-
* one spelling
|
|
1002
|
+
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
1003
|
+
* and one spelling.
|
|
1004
1004
|
* @param session - the session whose buffered events must reach durable storage.
|
|
1005
1005
|
* @returns whether at least one durability listener participated, after every
|
|
1006
1006
|
* listener has settled successfully.
|
package/lib/types/surface.js
CHANGED
|
@@ -80,14 +80,8 @@ export function deriveEventMessage(event, projectedMessages) {
|
|
|
80
80
|
// history; turn/step boundaries, failed attempts, and errors are trace/replay
|
|
81
81
|
// data.
|
|
82
82
|
switch (event.type) {
|
|
83
|
-
// Ordinary prompts and injected context
|
|
84
|
-
//
|
|
85
|
-
// (e.g. `<context>`) here: framing is caller-owned — a producer bakes it
|
|
86
|
-
// into `content`, as agent-instructions does with `<system-reminder>` — or,
|
|
87
|
-
// if reintroduced, must be driven by the event `meta` map and a dedicated
|
|
88
|
-
// renderer, keeping this projection a verbatim pass-through. See the
|
|
89
|
-
// deferred design note in
|
|
90
|
-
// ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md
|
|
83
|
+
// Ordinary prompts and injected context retain producer-owned framing.
|
|
84
|
+
// See Derived history in packages/core/session/README.md.
|
|
91
85
|
case 'user/message': {
|
|
92
86
|
return event.data;
|
|
93
87
|
}
|
package/lib/types/types.d.ts
CHANGED
|
@@ -417,8 +417,8 @@ export interface SessionEventMap {
|
|
|
417
417
|
* keep ordinary restore and replay lifecycle boundaries.
|
|
418
418
|
*
|
|
419
419
|
* Only the `Session` constructor and `buildForkSeed` may create this marker.
|
|
420
|
-
*
|
|
421
|
-
*
|
|
420
|
+
* Session append does not reject other writers, so a plugin appending one
|
|
421
|
+
* would silently classify every live bracket before it as seed history.
|
|
422
422
|
*
|
|
423
423
|
* An owner of a standalone open/close bracket (`compaction/start` …
|
|
424
424
|
* `compaction/end`) reads it because seed history and live work are otherwise
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session",
|
|
3
3
|
"description": "Event-sourced session store for the DeepSeek Harness",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1-alpha.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
"types": "./lib/types/index.d.ts",
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
|
-
"./invariant": {
|
|
22
|
-
"types": "./lib/types/invariant.d.ts",
|
|
23
|
-
"default": "./lib/invariant.js"
|
|
24
|
-
},
|
|
25
21
|
"./types": {
|
|
26
22
|
"types": "./lib/types/types.d.ts",
|
|
27
23
|
"default": "./lib/types/types.js"
|
|
@@ -39,25 +35,23 @@
|
|
|
39
35
|
},
|
|
40
36
|
"files": [
|
|
41
37
|
"lib/index.js",
|
|
42
|
-
"lib/invariant.js",
|
|
43
38
|
"lib/types/**/*.js",
|
|
44
39
|
"lib/types/**/*.d.ts"
|
|
45
40
|
],
|
|
46
41
|
"license": "MIT",
|
|
47
42
|
"peerDependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-scope": "0.2.
|
|
49
|
-
"@deepseek-ai/cordis": "~4.0.
|
|
43
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.1",
|
|
44
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1"
|
|
50
45
|
},
|
|
51
46
|
"devDependencies": {
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-typert-
|
|
55
|
-
"@deepseek-ai/
|
|
56
|
-
"@deepseek-ai/cordis": "~4.0.4"
|
|
47
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.1",
|
|
49
|
+
"@deepseek-ai/dsh-typert-registry": "0.2.1-alpha.1",
|
|
50
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1"
|
|
57
51
|
},
|
|
58
52
|
"dependencies": {
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
53
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.1",
|
|
54
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.1",
|
|
55
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.1"
|
|
62
56
|
}
|
|
63
57
|
}
|
package/lib/invariant.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
2
|
-
import "@deepseek-ai/dsh-brand";
|
|
3
|
-
//#endregion
|
|
4
|
-
//#region lib/types/invariant.js
|
|
5
|
-
/**
|
|
6
|
-
* Package-owned relational invariants for the session event log. Load this
|
|
7
|
-
* companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
|
|
8
|
-
*
|
|
9
|
-
* @module @deepseek-ai/dsh-session/invariant
|
|
10
|
-
*/
|
|
11
|
-
const PACKAGE_NAME = "@deepseek-ai/dsh-session";
|
|
12
|
-
/** Cordis companion plugin name. */
|
|
13
|
-
const name = "session-invariant";
|
|
14
|
-
/** Service required before the companion can reserve package ownership. */
|
|
15
|
-
const inject = ["invariants"];
|
|
16
|
-
/** Assert that a step-scoped event names the currently open turn and step. */
|
|
17
|
-
function requireOpenStep(trace, kind, turn, step, fail) {
|
|
18
|
-
if (trace.openTurn !== turn || trace.openStep !== step) fail(`${kind} names turn ${turn}/step ${step} but open is turn ${trace.openTurn}/step ${trace.openStep}`);
|
|
19
|
-
}
|
|
20
|
-
/** Validate one candidate event without mutating the committed trace. */
|
|
21
|
-
function validateEvent(trace, event, fail) {
|
|
22
|
-
if (event.seq <= trace.lastSeq) fail(`seq must strictly increase: saw ${event.seq} after ${trace.lastSeq}`);
|
|
23
|
-
let openTurn = trace.openTurn;
|
|
24
|
-
let openStep = trace.openStep;
|
|
25
|
-
let nextTurn = trace.nextTurn;
|
|
26
|
-
let nextStep = trace.nextStep;
|
|
27
|
-
let pendingCalls = { kind: "none" };
|
|
28
|
-
switch (event.type) {
|
|
29
|
-
case "turn/start":
|
|
30
|
-
if (trace.openTurn !== null) fail(`turn/start ${event.data.turn} while turn ${trace.openTurn} is still open`);
|
|
31
|
-
if (event.data.turn !== trace.nextTurn) fail(`turn/start expected turn ${trace.nextTurn}, got ${event.data.turn}`);
|
|
32
|
-
openTurn = event.data.turn;
|
|
33
|
-
nextStep = 1;
|
|
34
|
-
break;
|
|
35
|
-
case "turn/end":
|
|
36
|
-
if (trace.openTurn !== event.data.turn) fail(`turn/end ${event.data.turn} does not match open turn ${trace.openTurn}`);
|
|
37
|
-
if (trace.openStep !== null) fail(`turn/end ${event.data.turn} while step ${trace.openStep} is still open`);
|
|
38
|
-
openTurn = null;
|
|
39
|
-
nextTurn += 1;
|
|
40
|
-
break;
|
|
41
|
-
case "step/start":
|
|
42
|
-
if (trace.openTurn !== event.data.turn) fail(`step/start in turn ${event.data.turn} but open turn is ${trace.openTurn}`);
|
|
43
|
-
if (trace.openStep !== null) fail(`step/start ${event.data.step} while step ${trace.openStep} is still open`);
|
|
44
|
-
if (event.data.step !== trace.nextStep) fail(`step/start expected step ${trace.nextStep} in turn ${event.data.turn}, got ${event.data.step}`);
|
|
45
|
-
openStep = event.data.step;
|
|
46
|
-
break;
|
|
47
|
-
case "developer/message":
|
|
48
|
-
requireOpenStep(trace, "developer/message", event.data.turn, event.data.step, fail);
|
|
49
|
-
break;
|
|
50
|
-
case "step/end":
|
|
51
|
-
requireOpenStep(trace, "step/end", event.data.turn, event.data.step, fail);
|
|
52
|
-
pendingCalls = { kind: "clear" };
|
|
53
|
-
openStep = null;
|
|
54
|
-
nextStep += 1;
|
|
55
|
-
break;
|
|
56
|
-
case "assistant/attempt":
|
|
57
|
-
requireOpenStep(trace, "assistant/attempt", event.data.turn, event.data.step, fail);
|
|
58
|
-
break;
|
|
59
|
-
case "assistant/message":
|
|
60
|
-
requireOpenStep(trace, "assistant/message", event.data.turn, event.data.step, fail);
|
|
61
|
-
break;
|
|
62
|
-
case "tool/call":
|
|
63
|
-
requireOpenStep(trace, "tool/call", event.data.turn, event.data.step, fail);
|
|
64
|
-
pendingCalls = {
|
|
65
|
-
kind: "add",
|
|
66
|
-
callId: event.data.callId
|
|
67
|
-
};
|
|
68
|
-
break;
|
|
69
|
-
case "tool/result": {
|
|
70
|
-
if (event.surfaceOp !== "append") {
|
|
71
|
-
if (trace.openTurn === null) fail("tool/result surface replacement appended outside any open turn");
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
requireOpenStep(trace, "tool/result", event.data.turn, event.data.step, fail);
|
|
75
|
-
const callId = event.data.message.source.callId;
|
|
76
|
-
const syntheticNotStarted = event.data.message.isError === true && event.data.error?.code === "TOOL_NOT_STARTED";
|
|
77
|
-
if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) fail(`tool/result for ${callId} with no prior tool/call in this step`);
|
|
78
|
-
pendingCalls = {
|
|
79
|
-
kind: "delete",
|
|
80
|
-
callId
|
|
81
|
-
};
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
case "system/message":
|
|
85
|
-
requireOpenStep(trace, "system/message", event.data.turn, event.data.step, fail);
|
|
86
|
-
break;
|
|
87
|
-
case "user/message": break;
|
|
88
|
-
case "session/end-seed": break;
|
|
89
|
-
case "request/header":
|
|
90
|
-
case "request/context":
|
|
91
|
-
if (trace.openTurn === null) fail(`${event.type} appended outside any open turn (core execution events must be turn-enclosed)`);
|
|
92
|
-
break;
|
|
93
|
-
default: break;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
scalars: {
|
|
97
|
-
lastSeq: event.seq,
|
|
98
|
-
openTurn,
|
|
99
|
-
openStep,
|
|
100
|
-
nextTurn,
|
|
101
|
-
nextStep
|
|
102
|
-
},
|
|
103
|
-
pendingCalls
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/** Apply one already-validated transition after its event commits. */
|
|
107
|
-
function applyTransition(trace, transition) {
|
|
108
|
-
Object.assign(trace, transition.scalars);
|
|
109
|
-
switch (transition.pendingCalls.kind) {
|
|
110
|
-
case "none": break;
|
|
111
|
-
case "add":
|
|
112
|
-
trace.pendingCalls.add(transition.pendingCalls.callId);
|
|
113
|
-
break;
|
|
114
|
-
case "delete":
|
|
115
|
-
trace.pendingCalls.delete(transition.pendingCalls.callId);
|
|
116
|
-
break;
|
|
117
|
-
case "clear":
|
|
118
|
-
trace.pendingCalls.clear();
|
|
119
|
-
break;
|
|
120
|
-
/* v8 ignore next -- validateEvent produces this closed transition union */
|
|
121
|
-
default: assertNever(transition.pendingCalls, "session trace pending-call transition");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/** Install the session contribution into its child registration fiber. */
|
|
125
|
-
const install = Object.assign((ctx, fail) => {
|
|
126
|
-
const traces = /* @__PURE__ */ new WeakMap();
|
|
127
|
-
const stagedTransitions = /* @__PURE__ */ new WeakMap();
|
|
128
|
-
const freshTrace = () => ({
|
|
129
|
-
lastSeq: -1,
|
|
130
|
-
openTurn: null,
|
|
131
|
-
openStep: null,
|
|
132
|
-
nextTurn: 1,
|
|
133
|
-
nextStep: 1,
|
|
134
|
-
pendingCalls: /* @__PURE__ */ new Set()
|
|
135
|
-
});
|
|
136
|
-
const seedSession = (session) => {
|
|
137
|
-
const trace = freshTrace();
|
|
138
|
-
traces.set(session, trace);
|
|
139
|
-
for (const event of session.snapshotEvents()) applyTransition(trace, validateEvent(trace, event, fail));
|
|
140
|
-
return trace;
|
|
141
|
-
};
|
|
142
|
-
/* v8 ignore next -- session/event always follows list() or session/created seeding */
|
|
143
|
-
const traceFor = (session) => traces.get(session) ?? seedSession(session);
|
|
144
|
-
for (const session of ctx.sessions.list()) seedSession(session);
|
|
145
|
-
ctx.on("session/created", (session) => {
|
|
146
|
-
seedSession(session);
|
|
147
|
-
}, { global: true });
|
|
148
|
-
ctx.on("session/event", (session, event) => {
|
|
149
|
-
const staged = stagedTransitions.get(event);
|
|
150
|
-
/* v8 ignore next 2 -- internal/dispatch stages the exact callback arguments */
|
|
151
|
-
if (staged === void 0 || staged.session !== session) return fail("session/event reached publication without matching pre-commit validation");
|
|
152
|
-
stagedTransitions.delete(event);
|
|
153
|
-
applyTransition(staged.trace, staged.transition);
|
|
154
|
-
}, { global: true });
|
|
155
|
-
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
156
|
-
if (eventName !== "session/event") return;
|
|
157
|
-
const [session, event] = args;
|
|
158
|
-
const trace = traceFor(session);
|
|
159
|
-
const transition = validateEvent(trace, event, fail);
|
|
160
|
-
stagedTransitions.set(event, {
|
|
161
|
-
session,
|
|
162
|
-
trace,
|
|
163
|
-
transition
|
|
164
|
-
});
|
|
165
|
-
}, { global: true });
|
|
166
|
-
}, { inject: ["sessions"] });
|
|
167
|
-
/**
|
|
168
|
-
* Register the session invariant companion.
|
|
169
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
170
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
171
|
-
*/
|
|
172
|
-
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
173
|
-
//#endregion
|
|
174
|
-
export { apply, inject, name };
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package-owned relational invariants for the session event log. Load this
|
|
3
|
-
* companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
|
|
4
|
-
*
|
|
5
|
-
* @module @deepseek-ai/dsh-session/invariant
|
|
6
|
-
*/
|
|
7
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
-
/** Cordis companion plugin name. */
|
|
9
|
-
export declare const name = "session-invariant";
|
|
10
|
-
/** Service required before the companion can reserve package ownership. */
|
|
11
|
-
export declare const inject: string[];
|
|
12
|
-
/**
|
|
13
|
-
* Register the session invariant companion.
|
|
14
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
15
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
16
|
-
*/
|
|
17
|
-
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
18
|
-
//# sourceMappingURL=invariant.d.ts.map
|
package/lib/types/invariant.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package-owned relational invariants for the session event log. Load this
|
|
3
|
-
* companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
|
|
4
|
-
*
|
|
5
|
-
* @module @deepseek-ai/dsh-session/invariant
|
|
6
|
-
*/
|
|
7
|
-
import { assertNever } from '@deepseek-ai/dsh-util-values';
|
|
8
|
-
import { TOOL_NOT_STARTED } from "./repair.js";
|
|
9
|
-
const PACKAGE_NAME = '@deepseek-ai/dsh-session';
|
|
10
|
-
/** Cordis companion plugin name. */
|
|
11
|
-
export const name = 'session-invariant';
|
|
12
|
-
/** Service required before the companion can reserve package ownership. */
|
|
13
|
-
export const inject = ['invariants'];
|
|
14
|
-
/** Assert that a step-scoped event names the currently open turn and step. */
|
|
15
|
-
function requireOpenStep(trace, kind, turn, step, fail) {
|
|
16
|
-
if (trace.openTurn !== turn || trace.openStep !== step) {
|
|
17
|
-
fail(`${kind} names turn ${turn}/step ${step} but open is turn ${trace.openTurn}/step ${trace.openStep}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/** Validate one candidate event without mutating the committed trace. */
|
|
21
|
-
function validateEvent(trace, event, fail) {
|
|
22
|
-
if (event.seq <= trace.lastSeq) {
|
|
23
|
-
fail(`seq must strictly increase: saw ${event.seq} after ${trace.lastSeq}`);
|
|
24
|
-
}
|
|
25
|
-
let openTurn = trace.openTurn;
|
|
26
|
-
let openStep = trace.openStep;
|
|
27
|
-
let nextTurn = trace.nextTurn;
|
|
28
|
-
let nextStep = trace.nextStep;
|
|
29
|
-
let pendingCalls = { kind: 'none' };
|
|
30
|
-
// Context and plugin-owned log-only events may be appended between model
|
|
31
|
-
// executions. Core execution events retain their explicit turn relations.
|
|
32
|
-
switch (event.type) {
|
|
33
|
-
case 'turn/start': {
|
|
34
|
-
if (trace.openTurn !== null) {
|
|
35
|
-
fail(`turn/start ${event.data.turn} while turn ${trace.openTurn} is still open`);
|
|
36
|
-
}
|
|
37
|
-
if (event.data.turn !== trace.nextTurn) {
|
|
38
|
-
fail(`turn/start expected turn ${trace.nextTurn}, got ${event.data.turn}`);
|
|
39
|
-
}
|
|
40
|
-
openTurn = event.data.turn;
|
|
41
|
-
nextStep = 1;
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
case 'turn/end': {
|
|
45
|
-
if (trace.openTurn !== event.data.turn) {
|
|
46
|
-
fail(`turn/end ${event.data.turn} does not match open turn ${trace.openTurn}`);
|
|
47
|
-
}
|
|
48
|
-
if (trace.openStep !== null) {
|
|
49
|
-
fail(`turn/end ${event.data.turn} while step ${trace.openStep} is still open`);
|
|
50
|
-
}
|
|
51
|
-
openTurn = null;
|
|
52
|
-
nextTurn += 1;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
case 'step/start': {
|
|
56
|
-
if (trace.openTurn !== event.data.turn) {
|
|
57
|
-
fail(`step/start in turn ${event.data.turn} but open turn is ${trace.openTurn}`);
|
|
58
|
-
}
|
|
59
|
-
if (trace.openStep !== null) {
|
|
60
|
-
fail(`step/start ${event.data.step} while step ${trace.openStep} is still open`);
|
|
61
|
-
}
|
|
62
|
-
if (event.data.step !== trace.nextStep) {
|
|
63
|
-
fail(`step/start expected step ${trace.nextStep} in turn ${event.data.turn}, got ${event.data.step}`);
|
|
64
|
-
}
|
|
65
|
-
openStep = event.data.step;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
case 'developer/message': {
|
|
69
|
-
requireOpenStep(trace, 'developer/message', event.data.turn, event.data.step, fail);
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
case 'step/end': {
|
|
73
|
-
requireOpenStep(trace, 'step/end', event.data.turn, event.data.step, fail);
|
|
74
|
-
pendingCalls = { kind: 'clear' };
|
|
75
|
-
openStep = null;
|
|
76
|
-
nextStep += 1;
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
case 'assistant/attempt': {
|
|
80
|
-
requireOpenStep(trace, 'assistant/attempt', event.data.turn, event.data.step, fail);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
case 'assistant/message': {
|
|
84
|
-
requireOpenStep(trace, 'assistant/message', event.data.turn, event.data.step, fail);
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
case 'tool/call': {
|
|
88
|
-
requireOpenStep(trace, 'tool/call', event.data.turn, event.data.step, fail);
|
|
89
|
-
pendingCalls = { kind: 'add', callId: event.data.callId };
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
case 'tool/result': {
|
|
93
|
-
// Session has already validated a content rewrite that cites its replaced event.
|
|
94
|
-
// It is durable turn work, not a second execution of the original call.
|
|
95
|
-
if (event.surfaceOp !== 'append') {
|
|
96
|
-
if (trace.openTurn === null) {
|
|
97
|
-
fail('tool/result surface replacement appended outside any open turn');
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
requireOpenStep(trace, 'tool/result', event.data.turn, event.data.step, fail);
|
|
102
|
-
const callId = event.data.message.source.callId;
|
|
103
|
-
const syntheticNotStarted = event.data.message.isError === true && event.data.error?.code === TOOL_NOT_STARTED;
|
|
104
|
-
if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) {
|
|
105
|
-
fail(`tool/result for ${callId} with no prior tool/call in this step`);
|
|
106
|
-
}
|
|
107
|
-
pendingCalls = { kind: 'delete', callId };
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
case 'system/message': {
|
|
111
|
-
requireOpenStep(trace, 'system/message', event.data.turn, event.data.step, fail);
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
case 'user/message':
|
|
115
|
-
break;
|
|
116
|
-
case 'session/end-seed':
|
|
117
|
-
// Unconstrained: an unbalanced seed legally puts it inside an open turn.
|
|
118
|
-
break;
|
|
119
|
-
case 'request/header':
|
|
120
|
-
case 'request/context': {
|
|
121
|
-
if (trace.openTurn === null) {
|
|
122
|
-
fail(`${event.type} appended outside any open turn (core execution events must be turn-enclosed)`);
|
|
123
|
-
}
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
default:
|
|
127
|
-
// Merge-extensible event relations belong to their owning plugin.
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
scalars: { lastSeq: event.seq, openTurn, openStep, nextTurn, nextStep },
|
|
132
|
-
pendingCalls,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
/** Apply one already-validated transition after its event commits. */
|
|
136
|
-
function applyTransition(trace, transition) {
|
|
137
|
-
Object.assign(trace, transition.scalars);
|
|
138
|
-
switch (transition.pendingCalls.kind) {
|
|
139
|
-
case 'none':
|
|
140
|
-
break;
|
|
141
|
-
case 'add':
|
|
142
|
-
trace.pendingCalls.add(transition.pendingCalls.callId);
|
|
143
|
-
break;
|
|
144
|
-
case 'delete':
|
|
145
|
-
trace.pendingCalls.delete(transition.pendingCalls.callId);
|
|
146
|
-
break;
|
|
147
|
-
case 'clear':
|
|
148
|
-
trace.pendingCalls.clear();
|
|
149
|
-
break;
|
|
150
|
-
/* v8 ignore next -- validateEvent produces this closed transition union */
|
|
151
|
-
default:
|
|
152
|
-
assertNever(transition.pendingCalls, 'session trace pending-call transition');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/** Install the session contribution into its child registration fiber. */
|
|
156
|
-
const install = Object.assign((ctx, fail) => {
|
|
157
|
-
const traces = new WeakMap();
|
|
158
|
-
const stagedTransitions = new WeakMap();
|
|
159
|
-
const freshTrace = () => ({
|
|
160
|
-
lastSeq: -1,
|
|
161
|
-
openTurn: null,
|
|
162
|
-
openStep: null,
|
|
163
|
-
nextTurn: 1,
|
|
164
|
-
nextStep: 1,
|
|
165
|
-
pendingCalls: new Set(),
|
|
166
|
-
});
|
|
167
|
-
const seedSession = (session) => {
|
|
168
|
-
const trace = freshTrace();
|
|
169
|
-
traces.set(session, trace);
|
|
170
|
-
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
|
|
171
|
-
for (const event of session.snapshotEvents()) {
|
|
172
|
-
applyTransition(trace, validateEvent(trace, event, fail));
|
|
173
|
-
}
|
|
174
|
-
return trace;
|
|
175
|
-
};
|
|
176
|
-
/* v8 ignore next -- session/event always follows list() or session/created seeding */
|
|
177
|
-
const traceFor = (session) => traces.get(session) ?? seedSession(session);
|
|
178
|
-
for (const session of ctx.sessions.list())
|
|
179
|
-
seedSession(session);
|
|
180
|
-
ctx.on('session/created', (session) => { seedSession(session); }, { global: true });
|
|
181
|
-
ctx.on('session/event', (session, event) => {
|
|
182
|
-
const staged = stagedTransitions.get(event);
|
|
183
|
-
/* v8 ignore next 2 -- internal/dispatch stages the exact callback arguments */
|
|
184
|
-
if (staged === undefined || staged.session !== session) {
|
|
185
|
-
return fail('session/event reached publication without matching pre-commit validation');
|
|
186
|
-
}
|
|
187
|
-
stagedTransitions.delete(event);
|
|
188
|
-
applyTransition(staged.trace, staged.transition);
|
|
189
|
-
}, { global: true });
|
|
190
|
-
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
191
|
-
if (eventName !== 'session/event')
|
|
192
|
-
return;
|
|
193
|
-
const [session, event] = args;
|
|
194
|
-
const trace = traceFor(session);
|
|
195
|
-
const transition = validateEvent(trace, event, fail);
|
|
196
|
-
// A later dispatch listener may veto. Validation is pure, so abandoning
|
|
197
|
-
// this weakly keyed transition does not advance or retain the session.
|
|
198
|
-
stagedTransitions.set(event, { session, trace, transition });
|
|
199
|
-
}, { global: true });
|
|
200
|
-
}, { inject: ['sessions'] });
|
|
201
|
-
/**
|
|
202
|
-
* Register the session invariant companion.
|
|
203
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
204
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
205
|
-
*/
|
|
206
|
-
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
207
|
-
//# sourceMappingURL=invariant.js.map
|