@robiteame/dsh-tool-session-tree 0.1.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/README.i18n.yaml +6 -0
- package/README.md +98 -0
- package/README.zh.md +77 -0
- package/lib/index.js +491 -0
- package/lib/invariant.js +12 -0
- package/lib/types/index.d.ts +24 -0
- package/lib/types/invariant.d.ts +15 -0
- package/package.json +56 -0
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/extensions/tool-session-tree/README.md
|
|
5
|
+
README.md: 4f13ffd46a8a842a91bcc98a514deff06ac8b60b
|
|
6
|
+
README.zh.md: e00633ad46600dcb9755f7b23b407b20464af7b2
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# tool-session-tree
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Model-facing companion of the session-tree service: the `session_tree` tool,
|
|
6
|
+
the `/tree` command family, and a system-prompt section over the shared
|
|
7
|
+
append-only session-tree store. All state lives in
|
|
8
|
+
`sessionTreeStore` (`@robiteame/dsh-pi-agent-session-tree`), so the model,
|
|
9
|
+
the command line, and the browser panel observe the same trees.
|
|
10
|
+
|
|
11
|
+
## Surfaces
|
|
12
|
+
|
|
13
|
+
| Surface | Name | Notes |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Tool | `session_tree` | one tree per agent session; operations below |
|
|
16
|
+
| Command | `/tree` | Opens/refreshes the right sidebar; low-level subcommands remain available for automation |
|
|
17
|
+
| Commands | `/fork`, `/clone`, `/session` | Fork or clone the selected sidebar node with no IDs; inspect tree status |
|
|
18
|
+
|
|
19
|
+
## Tool operations
|
|
20
|
+
|
|
21
|
+
`create`, `append`, `list`, `branches`, `tree`, `jump`, `fork`, `clone`,
|
|
22
|
+
`context`, `session`, `branch`, `branch.summary`, `snapshot.save`,
|
|
23
|
+
`snapshot.load`, `sessions`.
|
|
24
|
+
|
|
25
|
+
- `context` returns `{cursor, messages}` — the standard LLM messages array for
|
|
26
|
+
the root→cursor path.
|
|
27
|
+
- `branch` parks the cursor at an existing node and names the next append's
|
|
28
|
+
branch; `branch.summary` additionally appends a summary node. Historical
|
|
29
|
+
nodes are never modified or deleted.
|
|
30
|
+
- `snapshot.save` returns the versioned snapshot; `snapshot.load` restores it
|
|
31
|
+
under the same sessionId (`version: 1`).
|
|
32
|
+
|
|
33
|
+
Every operation answers `{ok: true, value}` or
|
|
34
|
+
`{ok: false, error: {code, message}}`, so tool results are always lossless
|
|
35
|
+
JSON with a stable failure vocabulary. Human `/fork` and `/clone` commands
|
|
36
|
+
require a sidebar selection and otherwise return `请先在右侧会话树选中目标节点`.
|
|
37
|
+
|
|
38
|
+
## Tool examples
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{"operation":"create","sessionId":"demo"}
|
|
42
|
+
{"operation":"append","sessionId":"demo","message":{"role":"user","content":"Explore option A"}}
|
|
43
|
+
{"operation":"append","sessionId":"demo","message":{"role":"assistant","content":"Baseline answer"}}
|
|
44
|
+
{"operation":"branch","sessionId":"demo","nodeId":"<root-nodeId>","branch":"option-b"}
|
|
45
|
+
{"operation":"append","sessionId":"demo","message":{"role":"user","content":"Explore option B"},"branch":"option-b"}
|
|
46
|
+
{"operation":"context","sessionId":"demo"}
|
|
47
|
+
{"operation":"snapshot.save","sessionId":"demo"}
|
|
48
|
+
{"operation":"snapshot.load","sessionId":"demo","snapshot":{"version":1,"sessionId":"demo","cursor":null,"activeBranch":"main","nodes":[]}}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Model Experience
|
|
52
|
+
|
|
53
|
+
### System prompt
|
|
54
|
+
|
|
55
|
+
#### What the model sees
|
|
56
|
+
|
|
57
|
+
A fixed section tells the model that the tree projects the durable Harness Session log: native turns synchronize automatically, `context` reads the active branch after navigation, `append` is only for explicit custom entries, and branching never edits history. The `surface` field reported by `context` and `session` states the active surface mode (`native`, `stock`, or `projection`).
|
|
58
|
+
##### SessionTree guidance
|
|
59
|
+
|
|
60
|
+
```markdown
|
|
61
|
+
SessionTree is the append-only projection of this agent's durable Harness Session log. Native user, assistant, tool, and model-context events are synchronized automatically: never duplicate ordinary turns with operation 'append'. Before answering after navigation, call session_tree with operation 'context' and treat its root-to-cursor messages as the active branch context. Use 'append' only for an explicit custom tree entry not already recorded by Harness. To explore an alternative, call 'fork' or 'branch' with a historical nodeId and branch name (or 'branch.summary' to record a summary); old nodes are never modified or deleted. Use 'branches' and 'tree' to inspect topology, and 'snapshot.save'/'snapshot.load' for explicit export or full-tree restore. All operations report failures as {ok:false,error:{code,message}}. Depending on the Harness build, jump/fork switch the model-visible history either through the native selected-surface API or through an official replace-surface emulation; when neither is available the tree is projection-only. The surface field reported by 'context' and 'session' states the active mode: native, stock, or projection.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Token effect
|
|
65
|
+
|
|
66
|
+
Small fixed input cost on every request where this plugin's prompt registration is in scope, plus the returned JSON for each `context` call.
|
|
67
|
+
|
|
68
|
+
#### KV Cache effect
|
|
69
|
+
|
|
70
|
+
Prefix-stable while the plugin scope and guidance text are unchanged. The `context` result is dynamic output and never joins the reusable prefix.
|
|
71
|
+
|
|
72
|
+
### Tool schemas and results
|
|
73
|
+
|
|
74
|
+
#### What the model sees
|
|
75
|
+
|
|
76
|
+
The generated [`session_tree` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-session-tree) (one `operation` discriminant plus JSON payload fields). Successful results are compact JSON envelopes; a failed operation returns `{ok: false, error: {code, message}}` instead of throwing.
|
|
77
|
+
|
|
78
|
+
#### Token effect
|
|
79
|
+
|
|
80
|
+
Fixed schema cost plus one compact result per call.
|
|
81
|
+
|
|
82
|
+
#### KV Cache effect
|
|
83
|
+
|
|
84
|
+
Schemas are prefix-stable while their definitions and visibility are unchanged. Calls and results append after the reusable request prefix.
|
|
85
|
+
|
|
86
|
+
## Known Limitations and Deferred Work
|
|
87
|
+
|
|
88
|
+
- **Native events are durable** — the tree is rebuilt from the Harness Session
|
|
89
|
+
event log on live sync; explicit snapshots provide full-tree export and
|
|
90
|
+
restore, while the native model surface follows the active leaf.
|
|
91
|
+
- **Patched versus official Harness** — a patched checkout persists
|
|
92
|
+
`session-tree/*` markers and uses the selected-surface API. An official
|
|
93
|
+
Bundle appends an official `replace` surface event when the cursor moves and
|
|
94
|
+
persists branch metadata in the sidecar at
|
|
95
|
+
`$DSH_HOME/storages/session-tree/<sessionId>.json`.
|
|
96
|
+
- **The store is process-wide** — a restarted process reconstructs trees from
|
|
97
|
+
persisted Session events, and on an official Bundle additionally restores
|
|
98
|
+
the sidecar before the owning agent's next pre-step.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# tool-session-tree
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
会话树服务的模型面配套:`session_tree` 工具、`/tree` 命令族,以及共享只追加会话树存储之上的 system-prompt 片段。所有状态都在 `sessionTreeStore`(`@robiteame/dsh-pi-agent-session-tree`)中,因此模型、命令行与浏览器面板观察到同一批树。
|
|
6
|
+
|
|
7
|
+
## 表面
|
|
8
|
+
|
|
9
|
+
| 表面 | 名称 | 说明 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| 工具 | `session_tree` | 每个 agent 会话一棵树;操作见下 |
|
|
12
|
+
| 命令 | `/tree` | 打开或刷新右侧会话树;低级子命令继续供自动化使用 |
|
|
13
|
+
| 命令 | `/fork`、`/clone`、`/session` | 无需 ID,直接 fork/clone 右侧选中节点;查看树状态 |
|
|
14
|
+
|
|
15
|
+
## 工具操作
|
|
16
|
+
|
|
17
|
+
`create`、`append`、`list`、`branches`、`tree`、`jump`、`fork`、`clone`、`context`、`session`、`branch`、`branch.summary`、`snapshot.save`、`snapshot.load`、`sessions`。
|
|
18
|
+
|
|
19
|
+
- `context` 返回 `{cursor, messages}`——根→光标路径的标准 LLM messages 数组。
|
|
20
|
+
- `branch` 把光标停在已有节点并命名下一次 append 的分支;`branch.summary` 额外追加摘要节点。历史节点永不被修改或删除。
|
|
21
|
+
- `snapshot.save` 返回版本化快照;`snapshot.load` 在同一 sessionId 下恢复(`version: 1`)。
|
|
22
|
+
|
|
23
|
+
每次操作都返回 `{ok: true, value}` 或 `{ok: false, error: {code, message}}`,因此工具结果始终是无损 JSON,并带有稳定失败词表。用户执行 `/fork`、`/clone` 前必须先点击右侧节点,否则返回“请先在右侧会话树选中目标节点”。
|
|
24
|
+
|
|
25
|
+
## 工具示例
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{"operation":"create","sessionId":"demo"}
|
|
29
|
+
{"operation":"append","sessionId":"demo","message":{"role":"user","content":"Explore option A"}}
|
|
30
|
+
{"operation":"append","sessionId":"demo","message":{"role":"assistant","content":"Baseline answer"}}
|
|
31
|
+
{"operation":"branch","sessionId":"demo","nodeId":"<root-nodeId>","branch":"option-b"}
|
|
32
|
+
{"operation":"append","sessionId":"demo","message":{"role":"user","content":"Explore option B"},"branch":"option-b"}
|
|
33
|
+
{"operation":"context","sessionId":"demo"}
|
|
34
|
+
{"operation":"snapshot.save","sessionId":"demo"}
|
|
35
|
+
{"operation":"snapshot.load","sessionId":"demo","snapshot":{"version":1,"sessionId":"demo","cursor":null,"activeBranch":"main","nodes":[]}}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 模型体验
|
|
39
|
+
|
|
40
|
+
### 系统提示
|
|
41
|
+
|
|
42
|
+
#### 模型看到什么
|
|
43
|
+
|
|
44
|
+
一段固定文本告诉模型:树是 Harness 持久化 Session 日志的只追加投影——原生 user/assistant/tool/model 事件会自动同步;导航后先读 `context`,不要重复 append 普通轮次;仅用 `append` 记录明确的自定义 entry,从历史节点分叉且不修改旧节点。`context` 与 `session` 返回的 `surface` 字段报告当前表面模式(`native`、`stock` 或 `projection`)。
|
|
45
|
+
##### SessionTree 指引
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
SessionTree is the append-only projection of this agent's durable Harness Session log. Native user, assistant, tool, and model-context events are synchronized automatically: never duplicate ordinary turns with operation 'append'. Before answering after navigation, call session_tree with operation 'context' and treat its root-to-cursor messages as the active branch context. Use 'append' only for an explicit custom tree entry not already recorded by Harness. To explore an alternative, call 'fork' or 'branch' with a historical nodeId and branch name (or 'branch.summary' to record a summary); old nodes are never modified or deleted. Use 'branches' and 'tree' to inspect topology, and 'snapshot.save'/'snapshot.load' for explicit export or full-tree restore. All operations report failures as {ok:false,error:{code,message}}. Depending on the Harness build, jump/fork switch the model-visible history either through the native selected-surface API or through an official replace-surface emulation; when neither is available the tree is projection-only. The surface field reported by 'context' and 'session' states the active mode: native, stock, or projection.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Token 影响
|
|
52
|
+
|
|
53
|
+
本插件提示注册在作用域内时,每次请求的固定小成本输入,外加每次 `context` 调用返回的 JSON。
|
|
54
|
+
|
|
55
|
+
#### KV 缓存影响
|
|
56
|
+
|
|
57
|
+
在插件作用域与指引文本不变时前缀稳定。`context` 结果是动态输出,不进入可复用前缀。
|
|
58
|
+
|
|
59
|
+
### 工具 schema 与结果
|
|
60
|
+
|
|
61
|
+
#### 模型看到什么
|
|
62
|
+
|
|
63
|
+
生成的 [`session_tree` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-session-tree)(一个 `operation` 判别字段加 JSON 载荷字段)。成功结果是紧凑 JSON 信封;失败操作返回 `{ok: false, error: {code, message}}` 而不是抛错。
|
|
64
|
+
|
|
65
|
+
#### Token 影响
|
|
66
|
+
|
|
67
|
+
固定 schema 成本加每次调用一个紧凑结果。
|
|
68
|
+
|
|
69
|
+
#### KV 缓存影响
|
|
70
|
+
|
|
71
|
+
定义与可见性不变时 schema 前缀稳定。调用与结果追加在可复用请求前缀之后。
|
|
72
|
+
|
|
73
|
+
## 已知限制与待办
|
|
74
|
+
|
|
75
|
+
- **原生事件持久化**——每次实时同步都会从 Harness Session 事件日志重建树;显式快照用于整树导出与恢复,原生模型 surface 跟随活动叶子。
|
|
76
|
+
- **补丁集成与官方 Harness**——补丁集成持久化 `session-tree/*` 标记并使用 selected-surface API。官方 Bundle 在光标移动时追加官方 `replace` surface 事件,并把分支元数据保存到 `$DSH_HOME/storages/session-tree/<sessionId>.json`。
|
|
77
|
+
- **存储是进程级**——进程重启后,会在所属 Agent 恢复为实时状态时从持久 Session 事件重建树;官方 Bundle 还会在所属 Agent 下一次 pre-step 前恢复 sidecar。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// packages/extensions/tool-session-tree/src/index.ts
|
|
2
|
+
import { SessionId as toSessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import { appendSessionTreeEvent, applyTreeCursorToSession, isSessionTreeRestoreEvent, persistSessionTree, SessionTree, sessionTreeStore, sessionTreeSurfaceMode, supportsDurableSessionTreeEvents, syncSessionTree } from "@robiteame/dsh-pi-agent-session-tree";
|
|
5
|
+
var name = "tool-session-tree";
|
|
6
|
+
var inject = ["tools", "systemPrompt", "commands", "agents"];
|
|
7
|
+
var SESSION_TREE_PROMPT = "SessionTree is the append-only projection of this agent's durable Harness Session log. Native user, assistant, tool, and model-context events are synchronized automatically: never duplicate ordinary turns with operation 'append'. Before answering after navigation, call session_tree with operation 'context' and treat its root-to-cursor messages as the active branch context. Use 'append' only for an explicit custom tree entry not already recorded by Harness. To explore an alternative, call 'fork' or 'branch' with a historical nodeId and branch name (or 'branch.summary' to record a summary); old nodes are never modified or deleted. Use 'branches' and 'tree' to inspect topology, and 'snapshot.save'/'snapshot.load' for explicit export or full-tree restore. All operations report failures as {ok:false,error:{code,message}}. Depending on the Harness build, jump/fork switch the model-visible history either through the native selected-surface API or through an official replace-surface emulation; when neither is available the tree is projection-only. The surface field reported by 'context' and 'session' states the active mode: native, stock, or projection.";
|
|
8
|
+
function apply(ctx) {
|
|
9
|
+
ctx.systemPrompt.section({ name: "plugin:pi_agent_session_tree", order: 120, text: SESSION_TREE_PROMPT });
|
|
10
|
+
ctx.tools.register(defineTool({
|
|
11
|
+
name: "session_tree",
|
|
12
|
+
description: "Inspect and navigate the append-only tree projected from the durable Harness Session log. Native user/assistant/tool/model events synchronize automatically; use append only for an explicit custom entry, context to read the active cursor path, fork or branch from a historical node without deleting old history, clone the active path into an independent Harness session, and snapshot.save/snapshot.load for explicit export or restore.",
|
|
13
|
+
parameters: {
|
|
14
|
+
operation: { type: "string", required: true },
|
|
15
|
+
sessionId: { type: "string" },
|
|
16
|
+
nodeId: { type: "string" },
|
|
17
|
+
branch: { type: "string" },
|
|
18
|
+
summary: { type: "string" },
|
|
19
|
+
message: { type: "json" },
|
|
20
|
+
snapshot: { type: "json" },
|
|
21
|
+
metadata: { type: "json" },
|
|
22
|
+
targetSessionId: { type: "string" },
|
|
23
|
+
content: { type: "json" },
|
|
24
|
+
model: { type: "string" },
|
|
25
|
+
usage: { type: "json" },
|
|
26
|
+
cost: { type: "number" },
|
|
27
|
+
error: { type: "string" }
|
|
28
|
+
},
|
|
29
|
+
output: {
|
|
30
|
+
schema: { type: "json" },
|
|
31
|
+
render: (_args, value) => [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
32
|
+
},
|
|
33
|
+
async execute(args, exec) {
|
|
34
|
+
try {
|
|
35
|
+
return await runToolOperation(ctx, args, exec);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: { code: "INVALID_ARGUMENT", message: error instanceof Error ? error.message : "invalid request" }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
ctx.commands.register({
|
|
45
|
+
name: "tree",
|
|
46
|
+
description: "Open or refresh the right-sidebar session tree",
|
|
47
|
+
handler: (invocation) => runTreeCommand(ctx, invocation)
|
|
48
|
+
});
|
|
49
|
+
ctx.commands.register({ name: "fork", description: "Fork the selected session-tree node", handler: (invocation) => runForkCommand(invocation) });
|
|
50
|
+
ctx.commands.register({ name: "clone", description: "Clone the selected session-tree node into a new Harness session", handler: (invocation) => runCloneCommand(ctx, invocation) });
|
|
51
|
+
ctx.commands.register({ name: "session", description: "Show current session tree status", handler: (invocation) => runSessionCommand(invocation) });
|
|
52
|
+
}
|
|
53
|
+
async function runToolOperation(ctx, args, exec) {
|
|
54
|
+
if (exec.agent === void 0) {
|
|
55
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "session_tree requires an agent-backed session" } };
|
|
56
|
+
}
|
|
57
|
+
const sessionId = args.sessionId ?? exec.agent.session.id;
|
|
58
|
+
const isOwnSession = sessionId === exec.agent.session.id;
|
|
59
|
+
if (isOwnSession) syncSessionTree(exec.agent);
|
|
60
|
+
if (args.operation !== "sessions" && args.operation !== "clone" && !isOwnSession) {
|
|
61
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "session_tree can only modify the calling agent session" } };
|
|
62
|
+
}
|
|
63
|
+
if (args.operation === "clone" && args.targetSessionId === void 0) {
|
|
64
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "targetSessionId is required" } };
|
|
65
|
+
}
|
|
66
|
+
switch (args.operation) {
|
|
67
|
+
case "create": {
|
|
68
|
+
const tree2 = sessionTreeStore.get(sessionId) ?? sessionTreeStore.create(sessionId);
|
|
69
|
+
return { ok: true, value: { sessionId: tree2.sessionId } };
|
|
70
|
+
}
|
|
71
|
+
case "sessions":
|
|
72
|
+
return { ok: true, value: sessionTreeStore.list() };
|
|
73
|
+
case "clone": {
|
|
74
|
+
if (args.targetSessionId === void 0) return { ok: false, error: { code: "INVALID_ARGUMENT", message: "targetSessionId is required" } };
|
|
75
|
+
if (sessionId !== exec.agent.session.id) return { ok: false, error: { code: "INVALID_ARGUMENT", message: "clone must target the calling agent session" } };
|
|
76
|
+
return cloneActiveSession(ctx, exec.agent, args.targetSessionId);
|
|
77
|
+
}
|
|
78
|
+
case "snapshot.load": {
|
|
79
|
+
if (args.snapshot === void 0) {
|
|
80
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "snapshot is required" } };
|
|
81
|
+
}
|
|
82
|
+
const snapshot = args.snapshot;
|
|
83
|
+
if (snapshot.sessionId !== sessionId) {
|
|
84
|
+
return { ok: false, error: { code: "INVALID_SNAPSHOT", message: `snapshot session '${snapshot.sessionId}' does not match the calling session '${sessionId}'` } };
|
|
85
|
+
}
|
|
86
|
+
if (sessionId !== exec.agent.session.id) return { ok: false, error: { code: "INVALID_ARGUMENT", message: "snapshot.load requires the calling agent session" } };
|
|
87
|
+
const candidate = newSessionTreeFromSnapshot(sessionId, snapshot);
|
|
88
|
+
if (!candidate.ok) return { ok: false, error: { code: "INVALID_SNAPSHOT", message: candidate.error } };
|
|
89
|
+
const event = appendSessionTreeEvent(exec.agent.session, "session-tree/snapshot", { snapshot });
|
|
90
|
+
if (event !== void 0) candidate.value.markSessionEventSeq(event.seq);
|
|
91
|
+
sessionTreeStore.replace(sessionId, candidate.value);
|
|
92
|
+
applyTreeCursorToSession(exec.agent, candidate.value);
|
|
93
|
+
persistSessionTree(candidate.value);
|
|
94
|
+
return { ok: true, value: { sessionId } };
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
const required = sessionTreeStore.require(sessionId);
|
|
100
|
+
if (!required.ok) return required;
|
|
101
|
+
const tree = required.value;
|
|
102
|
+
switch (args.operation) {
|
|
103
|
+
case "append": {
|
|
104
|
+
if (args.message === void 0) {
|
|
105
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "message is required" } };
|
|
106
|
+
}
|
|
107
|
+
const request = args.message;
|
|
108
|
+
if (typeof request.role !== "string" || typeof request.content !== "string") {
|
|
109
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "message.role and message.content are required" } };
|
|
110
|
+
}
|
|
111
|
+
const checkpoint = tree.checkpoint();
|
|
112
|
+
const appended = tree.append(
|
|
113
|
+
{
|
|
114
|
+
role: request.role,
|
|
115
|
+
content: request.content,
|
|
116
|
+
...request.name === void 0 ? {} : { name: request.name },
|
|
117
|
+
...request.toolCallId === void 0 ? {} : { toolCallId: request.toolCallId }
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
...args.branch === void 0 ? {} : { branch: args.branch },
|
|
121
|
+
...args.summary === void 0 ? {} : { summary: args.summary },
|
|
122
|
+
...args.metadata === void 0 ? {} : { metadata: args.metadata },
|
|
123
|
+
...args.content === void 0 ? {} : { content: args.content },
|
|
124
|
+
...args.model === void 0 ? {} : { model: args.model },
|
|
125
|
+
...args.usage === void 0 ? {} : { usage: args.usage },
|
|
126
|
+
...args.cost === void 0 ? {} : { cost: args.cost },
|
|
127
|
+
...args.error === void 0 ? {} : { error: args.error }
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
if (appended.ok && sessionId === exec.agent.session.id) {
|
|
131
|
+
try {
|
|
132
|
+
const event = appendSessionTreeEvent(exec.agent.session, "session-tree/node", { node: appended.value });
|
|
133
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
134
|
+
persistSessionTree(tree);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
tree.rollback(checkpoint);
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return appended;
|
|
141
|
+
}
|
|
142
|
+
case "list":
|
|
143
|
+
return { ok: true, value: tree.list() };
|
|
144
|
+
case "branches":
|
|
145
|
+
return { ok: true, value: tree.branches() };
|
|
146
|
+
case "tree":
|
|
147
|
+
return { ok: true, value: tree.view() };
|
|
148
|
+
case "session":
|
|
149
|
+
return { ok: true, value: { ...tree.info(), surface: sessionTreeSurfaceMode(exec.agent.session) } };
|
|
150
|
+
case "jump": {
|
|
151
|
+
const checkpoint = tree.checkpoint();
|
|
152
|
+
const moved = tree.jump(args.nodeId ?? null);
|
|
153
|
+
if (moved.ok && sessionId === exec.agent.session.id) {
|
|
154
|
+
try {
|
|
155
|
+
const event = appendSessionTreeEvent(exec.agent.session, "session-tree/cursor", { nodeId: args.nodeId ?? null });
|
|
156
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
157
|
+
if (args.nodeId != null) {
|
|
158
|
+
const selected = tree.select(args.nodeId);
|
|
159
|
+
if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
|
|
160
|
+
const selection = appendSessionTreeEvent(exec.agent.session, "session-tree/selection", { nodeId: args.nodeId });
|
|
161
|
+
if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
|
|
162
|
+
}
|
|
163
|
+
applyTreeCursorToSession(exec.agent, tree);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
tree.rollback(checkpoint);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return moved;
|
|
170
|
+
}
|
|
171
|
+
case "fork": {
|
|
172
|
+
if (args.nodeId === void 0) return { ok: false, error: { code: "INVALID_ARGUMENT", message: "nodeId is required" } };
|
|
173
|
+
const checkpoint = tree.checkpoint();
|
|
174
|
+
const forked = tree.fork(args.nodeId, args.branch);
|
|
175
|
+
if (forked.ok && sessionId === exec.agent.session.id) {
|
|
176
|
+
try {
|
|
177
|
+
const selected = tree.select(args.nodeId);
|
|
178
|
+
if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
|
|
179
|
+
const event = appendSessionTreeEvent(exec.agent.session, "session-tree/branch", { nodeId: args.nodeId, branch: forked.value.branch });
|
|
180
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
181
|
+
const selection = appendSessionTreeEvent(exec.agent.session, "session-tree/selection", { nodeId: args.nodeId });
|
|
182
|
+
if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
|
|
183
|
+
applyTreeCursorToSession(exec.agent, tree);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
tree.rollback(checkpoint);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return forked;
|
|
190
|
+
}
|
|
191
|
+
case "context":
|
|
192
|
+
return { ok: true, value: { cursor: tree.cursor, surface: sessionTreeSurfaceMode(exec.agent.session), messages: tree.messages(args.nodeId ?? tree.cursor) } };
|
|
193
|
+
case "branch": {
|
|
194
|
+
if (args.nodeId === void 0 || args.branch === void 0) {
|
|
195
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "nodeId and branch are required" } };
|
|
196
|
+
}
|
|
197
|
+
const checkpoint = tree.checkpoint();
|
|
198
|
+
const branched = tree.branch(args.nodeId, args.branch);
|
|
199
|
+
if (branched.ok && sessionId === exec.agent.session.id) {
|
|
200
|
+
try {
|
|
201
|
+
const selected = tree.select(args.nodeId);
|
|
202
|
+
if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
|
|
203
|
+
const event = appendSessionTreeEvent(exec.agent.session, "session-tree/branch", { nodeId: args.nodeId, branch: args.branch });
|
|
204
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
205
|
+
const selection = appendSessionTreeEvent(exec.agent.session, "session-tree/selection", { nodeId: args.nodeId });
|
|
206
|
+
if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
|
|
207
|
+
applyTreeCursorToSession(exec.agent, tree);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
tree.rollback(checkpoint);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return branched;
|
|
214
|
+
}
|
|
215
|
+
case "branch.summary": {
|
|
216
|
+
if (args.nodeId === void 0 || args.summary === void 0) {
|
|
217
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: "nodeId and summary are required" } };
|
|
218
|
+
}
|
|
219
|
+
const checkpoint = tree.checkpoint();
|
|
220
|
+
const summarized = tree.branchWithSummary(args.nodeId, args.summary);
|
|
221
|
+
if (summarized.ok && sessionId === exec.agent.session.id) {
|
|
222
|
+
try {
|
|
223
|
+
const event = appendSessionTreeEvent(exec.agent.session, "session-tree/node", { node: summarized.value });
|
|
224
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
225
|
+
applyTreeCursorToSession(exec.agent, tree);
|
|
226
|
+
persistSessionTree(tree);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
tree.rollback(checkpoint);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return summarized;
|
|
233
|
+
}
|
|
234
|
+
case "snapshot.save":
|
|
235
|
+
return { ok: true, value: tree.snapshot() };
|
|
236
|
+
default:
|
|
237
|
+
return { ok: false, error: { code: "INVALID_ARGUMENT", message: `unknown operation '${args.operation}'` } };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function runForkCommand(invocation) {
|
|
241
|
+
const [branch = `fork-${Date.now().toString(36)}`] = invocation.rawInput.trim().split(/\s+/u).filter(Boolean);
|
|
242
|
+
const tree = syncSessionTree(invocation.agent);
|
|
243
|
+
if (tree.selectedNode === null) return errorCommand("\u8BF7\u5148\u5728\u53F3\u4FA7\u4F1A\u8BDD\u6811\u9009\u4E2D\u76EE\u6807\u8282\u70B9");
|
|
244
|
+
const checkpoint = tree.checkpoint();
|
|
245
|
+
const forked = tree.fork(tree.selectedNode, branch);
|
|
246
|
+
if (!forked.ok) return jsonCommand(forked);
|
|
247
|
+
try {
|
|
248
|
+
const selected = tree.select(forked.value.cursor);
|
|
249
|
+
if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
|
|
250
|
+
const event = appendSessionTreeEvent(invocation.agent.session, "session-tree/branch", { nodeId: forked.value.cursor, branch: forked.value.branch });
|
|
251
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
252
|
+
const selection = appendSessionTreeEvent(invocation.agent.session, "session-tree/selection", { nodeId: forked.value.cursor });
|
|
253
|
+
if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
|
|
254
|
+
applyTreeCursorToSession(invocation.agent, tree);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
tree.rollback(checkpoint);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
return jsonCommand(forked);
|
|
260
|
+
}
|
|
261
|
+
async function cloneActiveSession(ctx, agent, target, nodeId) {
|
|
262
|
+
const tree = syncSessionTree(agent);
|
|
263
|
+
const focusId = nodeId ?? tree.cursor;
|
|
264
|
+
const seed = agent.session.events.filter((event) => !isSessionTreeRestoreEvent(event)).map((event, index) => {
|
|
265
|
+
const record = JSON.parse(JSON.stringify(event));
|
|
266
|
+
if (record.type === "session-tree/snapshot") {
|
|
267
|
+
record.data = { ...record.data, snapshot: { ...record.data.snapshot, sessionId: toSessionId(target) } };
|
|
268
|
+
}
|
|
269
|
+
record.seq = index;
|
|
270
|
+
return record;
|
|
271
|
+
});
|
|
272
|
+
const seedTime = Date.now();
|
|
273
|
+
if (supportsDurableSessionTreeEvents(agent.session)) {
|
|
274
|
+
seed.push({
|
|
275
|
+
type: "session-tree/cursor",
|
|
276
|
+
seq: seed.length,
|
|
277
|
+
time: seedTime + seed.length,
|
|
278
|
+
data: { nodeId: focusId }
|
|
279
|
+
});
|
|
280
|
+
if (focusId !== null) {
|
|
281
|
+
seed.push({
|
|
282
|
+
type: "session-tree/selection",
|
|
283
|
+
seq: seed.length,
|
|
284
|
+
time: seedTime + seed.length,
|
|
285
|
+
data: { nodeId: focusId }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
seedCloneTree(toSessionId(target), tree, focusId, Math.max(seed.length - 1, 0));
|
|
290
|
+
try {
|
|
291
|
+
await ctx.agents.create({
|
|
292
|
+
sessionId: toSessionId(target),
|
|
293
|
+
seed,
|
|
294
|
+
meta: { parentSession: agent.session.id, seedLength: seed.length },
|
|
295
|
+
agentOptions: agent.options
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (!(error instanceof Error) || !error.message.includes("no agent factory registered")) {
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { ok: true, value: { sessionId: target } };
|
|
303
|
+
}
|
|
304
|
+
function seedCloneTree(targetId, source, focusId, nativeEventSeq) {
|
|
305
|
+
const snapshot = source.snapshot();
|
|
306
|
+
const focusNode = focusId === null ? void 0 : snapshot.nodes.find((node) => node.nodeId === focusId);
|
|
307
|
+
const activeBranch = focusNode?.branch ?? snapshot.activeBranch;
|
|
308
|
+
const branchHeads = { ...snapshot.branchHeads };
|
|
309
|
+
if (focusNode !== void 0) branchHeads[focusNode.branch] = focusNode.nodeId;
|
|
310
|
+
const clonedTree = new SessionTree(targetId, {
|
|
311
|
+
version: 1,
|
|
312
|
+
sessionId: targetId,
|
|
313
|
+
cursor: focusId,
|
|
314
|
+
activeBranch,
|
|
315
|
+
...focusId === null && Object.keys(branchHeads).length === 0 ? {} : { branchHeads },
|
|
316
|
+
selectedNodeId: focusId,
|
|
317
|
+
...nativeEventSeq < 0 ? {} : { nativeEventSeq },
|
|
318
|
+
nodes: snapshot.nodes
|
|
319
|
+
});
|
|
320
|
+
sessionTreeStore.replace(targetId, clonedTree);
|
|
321
|
+
persistSessionTree(clonedTree);
|
|
322
|
+
}
|
|
323
|
+
function newSessionTreeFromSnapshot(sessionId, snapshot) {
|
|
324
|
+
try {
|
|
325
|
+
return { ok: true, value: new SessionTree(sessionId, snapshot) };
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return { ok: false, error: error instanceof Error ? error.message : "invalid snapshot" };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function runCloneCommand(ctx, invocation) {
|
|
331
|
+
const tree = syncSessionTree(invocation.agent);
|
|
332
|
+
if (tree.selectedNode === null) return errorCommand("\u8BF7\u5148\u5728\u53F3\u4FA7\u4F1A\u8BDD\u6811\u9009\u4E2D\u76EE\u6807\u8282\u70B9");
|
|
333
|
+
const target = `${invocation.agent.session.id}-clone-${Date.now().toString(36)}`;
|
|
334
|
+
try {
|
|
335
|
+
return jsonCommand(await cloneActiveSession(ctx, invocation.agent, target, tree.selectedNode));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return { kind: "error", text: JSON.stringify({ ok: false, error: { code: "INVALID_ARGUMENT", message: error instanceof Error ? error.message : "clone failed" } }) };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function runSessionCommand(invocation) {
|
|
341
|
+
return jsonCommand({ ok: true, value: { ...syncSessionTree(invocation.agent).info(), surface: sessionTreeSurfaceMode(invocation.agent.session) } });
|
|
342
|
+
}
|
|
343
|
+
function jsonCommand(value, map) {
|
|
344
|
+
const result = map === void 0 ? value : value.ok ? map(value.value) : value;
|
|
345
|
+
const failed = typeof result === "object" && result !== null && result.ok === false;
|
|
346
|
+
return { kind: failed ? "error" : "success", text: JSON.stringify(result) };
|
|
347
|
+
}
|
|
348
|
+
function errorCommand(message) {
|
|
349
|
+
return { kind: "error", text: JSON.stringify({ ok: false, error: { code: "INVALID_ARGUMENT", message } }) };
|
|
350
|
+
}
|
|
351
|
+
async function runTreeCommand(ctx, invocation) {
|
|
352
|
+
const parts = invocation.rawInput.trim().split(/\s+/u).filter(Boolean);
|
|
353
|
+
const sessionId = invocation.agent.session.id;
|
|
354
|
+
const tree = syncSessionTree(invocation.agent);
|
|
355
|
+
const surface = sessionTreeSurfaceMode(invocation.agent.session);
|
|
356
|
+
const [head, ...rest] = parts;
|
|
357
|
+
const action = head === "snapshot" ? `snapshot.${rest[0] ?? ""}` : head ?? "list";
|
|
358
|
+
const json = (value) => {
|
|
359
|
+
const failed = typeof value === "object" && value !== null && value.ok === false;
|
|
360
|
+
return { kind: failed ? "error" : "success", text: JSON.stringify(value) };
|
|
361
|
+
};
|
|
362
|
+
switch (action) {
|
|
363
|
+
// Spreading the node array would collapse it into numeric keys; keep it a
|
|
364
|
+
// real array under `nodes` so JSON consumers can iterate it.
|
|
365
|
+
case "list":
|
|
366
|
+
return json({ ok: true, value: { nodes: tree.list(), surface } });
|
|
367
|
+
case "branches":
|
|
368
|
+
return json({ ok: true, value: tree.branches() });
|
|
369
|
+
case "tree":
|
|
370
|
+
return json({ ok: true, value: { ...tree.view(), surface } });
|
|
371
|
+
case "session":
|
|
372
|
+
return json({ ok: true, value: { ...tree.info(), surface } });
|
|
373
|
+
case "fork": {
|
|
374
|
+
if (tree.selectedNode === null) return json({ ok: false, error: { code: "INVALID_ARGUMENT", message: "\u8BF7\u5148\u5728\u53F3\u4FA7\u4F1A\u8BDD\u6811\u9009\u4E2D\u76EE\u6807\u8282\u70B9" } });
|
|
375
|
+
const checkpoint = tree.checkpoint();
|
|
376
|
+
const result = tree.fork(tree.selectedNode, rest[0] ?? "fork");
|
|
377
|
+
if (result.ok) {
|
|
378
|
+
try {
|
|
379
|
+
const selected = tree.select(result.value.cursor);
|
|
380
|
+
if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
|
|
381
|
+
const event = appendSessionTreeEvent(invocation.agent.session, "session-tree/branch", { nodeId: result.value.cursor, branch: result.value.branch });
|
|
382
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
383
|
+
const selection = appendSessionTreeEvent(invocation.agent.session, "session-tree/selection", { nodeId: result.value.cursor });
|
|
384
|
+
if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
|
|
385
|
+
applyTreeCursorToSession(invocation.agent, tree);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
tree.rollback(checkpoint);
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return json(result);
|
|
392
|
+
}
|
|
393
|
+
case "clone": {
|
|
394
|
+
if (tree.selectedNode === null) return json({ ok: false, error: { code: "INVALID_ARGUMENT", message: "\u8BF7\u5148\u5728\u53F3\u4FA7\u4F1A\u8BDD\u6811\u9009\u4E2D\u76EE\u6807\u8282\u70B9" } });
|
|
395
|
+
const target = rest[0] ?? `${sessionId}-clone-${Date.now().toString(36)}`;
|
|
396
|
+
try {
|
|
397
|
+
return json(await cloneActiveSession(ctx, invocation.agent, target, tree.selectedNode));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return json({ ok: false, error: { code: "INVALID_ARGUMENT", message: error instanceof Error ? error.message : "clone failed" } });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
case "context":
|
|
403
|
+
return json({ ok: true, value: { cursor: tree.cursor, selectedNodeId: tree.selectedNode, surface, messages: tree.messages(tree.selectedNode ?? tree.cursor) } });
|
|
404
|
+
case "jump": {
|
|
405
|
+
const nodeId = rest[0] ?? tree.selectedNode;
|
|
406
|
+
if (nodeId === null) return json({ ok: false, error: { code: "INVALID_ARGUMENT", message: "\u8BF7\u5148\u5728\u53F3\u4FA7\u4F1A\u8BDD\u6811\u9009\u4E2D\u76EE\u6807\u8282\u70B9" } });
|
|
407
|
+
const checkpoint = tree.checkpoint();
|
|
408
|
+
const result = tree.jump(nodeId);
|
|
409
|
+
if (result.ok) {
|
|
410
|
+
try {
|
|
411
|
+
const event = appendSessionTreeEvent(invocation.agent.session, "session-tree/cursor", { nodeId });
|
|
412
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
413
|
+
const selected = tree.select(nodeId);
|
|
414
|
+
if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
|
|
415
|
+
const selection = appendSessionTreeEvent(invocation.agent.session, "session-tree/selection", { nodeId });
|
|
416
|
+
if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
|
|
417
|
+
applyTreeCursorToSession(invocation.agent, tree);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
tree.rollback(checkpoint);
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return json(result);
|
|
424
|
+
}
|
|
425
|
+
case "branch": {
|
|
426
|
+
const nodeId = tree.selectedNode ?? "";
|
|
427
|
+
const branch = rest[0] ?? "fork";
|
|
428
|
+
const checkpoint = tree.checkpoint();
|
|
429
|
+
const result = tree.branch(nodeId, branch);
|
|
430
|
+
if (result.ok) {
|
|
431
|
+
try {
|
|
432
|
+
const selected = tree.select(nodeId);
|
|
433
|
+
if (!selected.ok) throw new Error(`${selected.error.code}: ${selected.error.message}`);
|
|
434
|
+
const event = appendSessionTreeEvent(invocation.agent.session, "session-tree/branch", { nodeId, branch });
|
|
435
|
+
if (event !== void 0) tree.markSessionEventSeq(event.seq);
|
|
436
|
+
const selection = appendSessionTreeEvent(invocation.agent.session, "session-tree/selection", { nodeId });
|
|
437
|
+
if (selection !== void 0) tree.markSessionEventSeq(selection.seq);
|
|
438
|
+
applyTreeCursorToSession(invocation.agent, tree);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
tree.rollback(checkpoint);
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return json(result);
|
|
445
|
+
}
|
|
446
|
+
case "snapshot.save":
|
|
447
|
+
return json({ ok: true, value: tree.snapshot() });
|
|
448
|
+
case "snapshot.load": {
|
|
449
|
+
const raw = rest.slice(1).join(" ").trim();
|
|
450
|
+
if (raw === "") {
|
|
451
|
+
return { kind: "error", text: JSON.stringify({ ok: false, error: { code: "INVALID_ARGUMENT", message: "snapshot JSON is required" } }) };
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const snapshot = JSON.parse(raw);
|
|
455
|
+
if (snapshot.sessionId !== sessionId) {
|
|
456
|
+
return json({ ok: false, error: { code: "INVALID_SNAPSHOT", message: `snapshot session '${snapshot.sessionId}' does not match the calling session '${sessionId}'` } });
|
|
457
|
+
}
|
|
458
|
+
const candidate = new SessionTree(sessionId, snapshot);
|
|
459
|
+
try {
|
|
460
|
+
const event = appendSessionTreeEvent(invocation.agent.session, "session-tree/snapshot", { snapshot });
|
|
461
|
+
if (event !== void 0) candidate.markSessionEventSeq(event.seq);
|
|
462
|
+
sessionTreeStore.replace(sessionId, candidate);
|
|
463
|
+
applyTreeCursorToSession(invocation.agent, candidate);
|
|
464
|
+
persistSessionTree(candidate);
|
|
465
|
+
return json({ ok: true, value: { sessionId } });
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
return { kind: "error", text: JSON.stringify({ ok: false, error: { code: "INVALID_SNAPSHOT", message: error instanceof Error ? error.message : "invalid snapshot JSON" } }) };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
default:
|
|
474
|
+
return {
|
|
475
|
+
kind: "error",
|
|
476
|
+
text: JSON.stringify({
|
|
477
|
+
ok: false,
|
|
478
|
+
error: {
|
|
479
|
+
code: "INVALID_ARGUMENT",
|
|
480
|
+
message: "Usage: /tree [list|branches|tree|context|jump <nodeId>|branch <nodeId> <name>|snapshot save|snapshot load <json>]"
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
export {
|
|
487
|
+
SESSION_TREE_PROMPT,
|
|
488
|
+
apply,
|
|
489
|
+
inject,
|
|
490
|
+
name
|
|
491
|
+
};
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// packages/extensions/tool-session-tree/src/invariant.ts
|
|
2
|
+
var PACKAGE_NAME = "@robiteame/dsh-tool-session-tree";
|
|
3
|
+
var name = "tool-session-tree-invariant";
|
|
4
|
+
var inject = ["invariants"];
|
|
5
|
+
var install = () => {
|
|
6
|
+
};
|
|
7
|
+
var apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
8
|
+
export {
|
|
9
|
+
apply,
|
|
10
|
+
inject,
|
|
11
|
+
name
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing tool and slash-command companion of the session-tree service:
|
|
3
|
+
* the `session_tree` tool, the `/tree` command family, and a system-prompt
|
|
4
|
+
* section. All state lives in the shared `sessionTreeStore` from
|
|
5
|
+
* `@robiteame/dsh-pi-agent-session-tree`, so the model, the command line,
|
|
6
|
+
* and the browser panel observe the same trees.
|
|
7
|
+
*
|
|
8
|
+
* Design notes
|
|
9
|
+
* - Harness Session events remain the durable history source. The prompt tells
|
|
10
|
+
* the model to read the active branch with `context`, never duplicate native
|
|
11
|
+
* turns with `append`, and never edit historical nodes.
|
|
12
|
+
* - Every operation returns the standard `{ok, value}|{ok:false,error}` JSON
|
|
13
|
+
* envelope; a thrown body also settles as that envelope, so tool results
|
|
14
|
+
* are always lossless JSON.
|
|
15
|
+
*
|
|
16
|
+
* @module @robiteame/dsh-tool-session-tree
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
export declare const name = "tool-session-tree";
|
|
20
|
+
export declare const inject: string[];
|
|
21
|
+
/** System-prompt fragment steering the agent to the tree as its only history. */
|
|
22
|
+
export declare const SESSION_TREE_PROMPT = "SessionTree is the append-only projection of this agent's durable Harness Session log. Native user, assistant, tool, and model-context events are synchronized automatically: never duplicate ordinary turns with operation 'append'. Before answering after navigation, call session_tree with operation 'context' and treat its root-to-cursor messages as the active branch context. Use 'append' only for an explicit custom tree entry not already recorded by Harness. To explore an alternative, call 'fork' or 'branch' with a historical nodeId and branch name (or 'branch.summary' to record a summary); old nodes are never modified or deleted. Use 'branches' and 'tree' to inspect topology, and 'snapshot.save'/'snapshot.load' for explicit export or full-tree restore. All operations report failures as {ok:false,error:{code,message}}. Depending on the Harness build, jump/fork switch the model-visible history either through the native selected-surface API or through an official replace-surface emulation; when neither is available the tree is projection-only. The surface field reported by 'context' and 'session' states the active mode: native, stock, or projection.";
|
|
23
|
+
/** Register the tool, the command family, and the prompt section. */
|
|
24
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@robiteame/dsh-tool-session-tree`.
|
|
3
|
+
* @module @robiteame/dsh-tool-session-tree/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "tool-session-tree-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@robiteame/dsh-tool-session-tree",
|
|
3
|
+
"description": "session_tree tool, /tree command family, and system-prompt section over the shared append-only session-tree store",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/robiteame/dsh-session-tree-extension.git",
|
|
11
|
+
"directory": "packages/extensions/tool-session-tree"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/robiteame/dsh-session-tree-extension/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./invariant": {
|
|
25
|
+
"types": "./lib/types/invariant.d.ts",
|
|
26
|
+
"default": "./lib/invariant.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib/index.js",
|
|
32
|
+
"lib/invariant.js",
|
|
33
|
+
"lib/types/**/*.d.ts",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "node ../../../scripts/build.mjs tool",
|
|
38
|
+
"watch": "node ../../../scripts/build.mjs tool"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
43
|
+
"@deepseek-ai/dsh-commands": "^0.1.2-alpha.3",
|
|
44
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
|
|
45
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
|
|
46
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
|
|
47
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.3",
|
|
48
|
+
"@robiteame/dsh-pi-agent-session-tree": "^0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@robiteame/dsh-pi-agent-session-tree": "^0.1.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=22"
|
|
55
|
+
}
|
|
56
|
+
}
|