@4fu/dsh-arbor-driver 0.0.0-stage → 2026.1110.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/LICENSE +21 -0
- package/README.md +234 -2
- package/assistant-stream.d.ts +47 -0
- package/assistant-stream.js +98 -0
- package/driver.d.ts +212 -0
- package/driver.js +885 -0
- package/inbox.d.ts +55 -0
- package/inbox.js +160 -0
- package/index.d.ts +18 -0
- package/index.js +14 -0
- package/package.json +74 -4
- package/registry.d.ts +57 -0
- package/registry.js +95 -0
- package/relay.d.ts +51 -0
- package/relay.js +144 -0
- package/runtime-context.d.ts +49 -0
- package/runtime-context.js +86 -0
- package/session-agent.d.ts +142 -0
- package/session-agent.js +467 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 4fuu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,235 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-arbor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run [arbor](https://github.com/4fuu/arbor) as the agent driver inside
|
|
4
|
+
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).
|
|
5
|
+
|
|
6
|
+
DSH gives a user a session, a transcript, approvals, plugins, skills and a model
|
|
7
|
+
picker. arbor gives an agent disposable context windows over one shared journal,
|
|
8
|
+
note store and workspace, instead of one long conversation. dsh-arbor connects
|
|
9
|
+
them: one arbor host runs inside the DSH process, each DSH session that selects
|
|
10
|
+
**Arbor** is served by that host, and the window's reply is written back to the
|
|
11
|
+
DSH session as an ordinary assistant message. A DSH session that does not select
|
|
12
|
+
Arbor keeps running on DSH's own embedded AgentLoop, in the same process.
|
|
13
|
+
|
|
14
|
+
> **Status.** This is a research-grade integration, not a DSH release. It is
|
|
15
|
+
> pinned to one DSH version and relies on DSH internals. Read
|
|
16
|
+
> [Versions and compatibility](docs/versions.md) before installing it on
|
|
17
|
+
> anything you care about. The packages are published under `@4fu` at
|
|
18
|
+
> `2026.1110.0`; the bundle and the font plugin install independently from npm.
|
|
19
|
+
|
|
20
|
+
## What an arbor session changes
|
|
21
|
+
|
|
22
|
+
Choosing **Arbor** in a DSH composer creates the session under the `arbor` agent
|
|
23
|
+
preset. That session then runs on arbor, and the following are true for it:
|
|
24
|
+
|
|
25
|
+
- **One window per turn, one chain per session.** A DSH session is one arbor
|
|
26
|
+
conversation: its first turn opens a window, a window that ends hands off to a
|
|
27
|
+
continuation, and the next turn drives the chain. The journal and note store
|
|
28
|
+
live under `$DSH_HOME/arbor/` and survive a restart.
|
|
29
|
+
- **arbor's workspace, not DSH's sandbox.** A window works in arbor's shared
|
|
30
|
+
folder (`$DSH_HOME/arbor/shared`). When the `ovnfs` binary is available a
|
|
31
|
+
window opens a copy-on-write checkout and merges back with `apply`; otherwise
|
|
32
|
+
arbor uses a plain directory. **There is no DSH sandbox for an arbor session:**
|
|
33
|
+
arbor's file tools and commands run on the host, and arbor's own secret scrub
|
|
34
|
+
is the only protection on what reaches the model.
|
|
35
|
+
- **arbor's projects.** The shared folder's project folders are the DSH project
|
|
36
|
+
list for arbor composers. A directory the user adds outside the shared folder
|
|
37
|
+
is recorded as an external pointer for that session and used in place.
|
|
38
|
+
- **arbor's prompt and tools.** DSH's official prompt sections are decided one by
|
|
39
|
+
one; the ones arbor replaces are absent, third-party sections go into a
|
|
40
|
+
`<dsh>` data block, and a trusted `<harness>` section says what remains true of
|
|
41
|
+
DSH. DSH tools are offered to the model as arbor external tools, and arbor's
|
|
42
|
+
own tools replace DSH tools with the same capability.
|
|
43
|
+
- **The Arbor page.** A permanent **Arbor** entry in DSH's left navigation opens
|
|
44
|
+
the whole-host overview and one tab per arbor session: windows, chains,
|
|
45
|
+
handoffs, projects, the board, notes, `/env`, schedules and arbor's search.
|
|
46
|
+
An arbor composer hides DSH's sandbox access-mode switch, because that switch
|
|
47
|
+
does not apply to it.
|
|
48
|
+
|
|
49
|
+
## What stays untouched in ordinary DSH sessions
|
|
50
|
+
|
|
51
|
+
A session that does not select Arbor is an ordinary DSH session. It keeps:
|
|
52
|
+
|
|
53
|
+
- DSH's embedded AgentLoop, session format, turn boundaries and streaming path;
|
|
54
|
+
- DSH's sandbox policy and its `ctx.fs` / `ctx.shell` file and command tools;
|
|
55
|
+
- DSH's project management, working directory and prompt assembly;
|
|
56
|
+
- its composer, sandbox access-mode switch, header and sidebar behavior.
|
|
57
|
+
|
|
58
|
+
It gets **no Arbor tab** and no arbor windows. The only shared surface is the
|
|
59
|
+
permanent Arbor entry in the left navigation, which simply does not list that
|
|
60
|
+
session. The routing registry replaces DSH's base `agent` row so it can send
|
|
61
|
+
`agentPreset: 'arbor'` sessions to arbor; every other session goes to the
|
|
62
|
+
official loop. Approvals still use DSH's approval path for both kinds of
|
|
63
|
+
session, and arbor's windows are registered so DSH's sidebar leaves them out.
|
|
64
|
+
|
|
65
|
+
## Screenshots
|
|
66
|
+
|
|
67
|
+
| Overview, light | Overview, dark |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
|  |  |
|
|
70
|
+
| **Session tab, light** | **Session tab, dark** |
|
|
71
|
+
|  |  |
|
|
72
|
+
|
|
73
|
+
These are the web profile at 1440 px wide with the optional
|
|
74
|
+
[font plugin](https://github.com/4fuu/dsh-arbor-fonts) installed: ChillDIN
|
|
75
|
+
Gothic for labels and ChillDIN for numerals, while body text and code keep
|
|
76
|
+
DSH's fonts. The same overview without the plugin is
|
|
77
|
+
[here](docs/screenshots/overview-without-fonts.png).
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
Requirements: Node.js 24.9 or newer and DeepSeek Harness `dsh-v0.2.2-rc.1`.
|
|
82
|
+
The bundle is published with all packages it depends on; nothing has to be
|
|
83
|
+
built or linked.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install -g @deepseek-ai/dsh@0.2.2-rc.1
|
|
87
|
+
dsh plugin --profile web add @4fu/dsh-arbor-bundle@2026.1110.0
|
|
88
|
+
dsh web
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`dsh plugin` adds the bundle to the profile's `dsh.profile.bundles` and installs
|
|
92
|
+
its packages into the profile. Pick **Arbor** in a composer and send a message.
|
|
93
|
+
The optional [font plugin](https://github.com/4fuu/dsh-arbor-fonts) installs the
|
|
94
|
+
same way:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
dsh plugin --profile web add @4fu/dsh-arbor-fonts@2026.1110.0
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### From a checkout
|
|
101
|
+
|
|
102
|
+
The proofs and development use the TypeScript sources directly; that checkout is
|
|
103
|
+
what the scripts and the pin check run against.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/4fuu/dsh-arbor
|
|
107
|
+
cd dsh-arbor
|
|
108
|
+
pnpm install --frozen-lockfile
|
|
109
|
+
pnpm check # tsc --noEmit over every package and proof
|
|
110
|
+
pnpm proofs # 29 proofs against the scripted model
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The first install may ask pnpm to approve dependency build scripts. Do not
|
|
114
|
+
approve them by hand: the repository's `pnpm-workspace.yaml` already denies the
|
|
115
|
+
six scripts it pulls in, each with the reason, and `pnpm install` finishes
|
|
116
|
+
cleanly with that policy.
|
|
117
|
+
|
|
118
|
+
### Hand the install to a model
|
|
119
|
+
|
|
120
|
+
[docs/install-for-agents.md](docs/install-for-agents.md) is a step-by-step guide
|
|
121
|
+
written for an AI agent: install from npm or a checkout, run the proofs, drive a
|
|
122
|
+
live turn with a restart, boot the web profile, and inspect what ran. Give a
|
|
123
|
+
model this prompt, editing the bracketed parts or dropping them:
|
|
124
|
+
|
|
125
|
+
```text copy Copy Install Prompt
|
|
126
|
+
Install dsh-arbor (https://github.com/4fuu/dsh-arbor) in this environment [in a
|
|
127
|
+
scratch DSH home under /tmp] and show me that it works: follow
|
|
128
|
+
docs/install-for-agents.md, install @4fu/dsh-arbor-bundle@2026.1110.0 into a DSH
|
|
129
|
+
web profile, pick Arbor for one session, and [also run the proof suite from a
|
|
130
|
+
checkout with pnpm install --frozen-lockfile, pnpm check and pnpm proofs]. Report
|
|
131
|
+
which checks passed, what the live session answered, and anything that did not
|
|
132
|
+
work. Fix only what is yours to fix and tell me what you changed.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Use it in DSH
|
|
136
|
+
|
|
137
|
+
The npm install above adds the bundle to a DSH web profile. Pick **Arbor** in a
|
|
138
|
+
composer and send a message: the session is created under the preset, its window
|
|
139
|
+
and chain appear under the **Arbor** entry, and an ordinary session started
|
|
140
|
+
beside it keeps DSH's own composer and sandbox behavior. A checkout can also be
|
|
141
|
+
wired by hand; [section 7 of the install guide](docs/install-for-agents.md#7-wiring-the-bundle-into-a-real-dsh-installation)
|
|
142
|
+
shows the profile layout.
|
|
143
|
+
|
|
144
|
+
The one-shot `dsh <task>` runner cannot drive an arbor-preset session: DSH's
|
|
145
|
+
runner refuses to adopt a session that runs under a preset. Use the web client,
|
|
146
|
+
the SDK path the proofs use, or a remote `agent-preset/select` on a blank.
|
|
147
|
+
|
|
148
|
+
## Pinned DSH version
|
|
149
|
+
|
|
150
|
+
DSH is pinned to **`dsh-v0.2.2-rc.1`** (npm
|
|
151
|
+
`@deepseek-ai/dsh@0.2.2-rc.1`, commit `66491b05e3` of
|
|
152
|
+
`deepseek-ai/deepseek-harness`). Every `@deepseek-ai/dsh*` dependency is an
|
|
153
|
+
exact version, every package under `packages/` declares
|
|
154
|
+
`engines.dsh: 0.2.2-rc.1`, and `pnpm check:pin` checks both plus the prompt
|
|
155
|
+
bridge's `DSH_PINNED_VERSION`.
|
|
156
|
+
|
|
157
|
+
A different DSH version is unsupported. The bundle depends on DSH's row names,
|
|
158
|
+
its one-`AgentFactory` rule, its prompt-section registry, its client slot
|
|
159
|
+
registration and its exact Fetch registry; a move in any of them can fail the
|
|
160
|
+
boot, leave an official prompt section undecided, or stop the Arbor page from
|
|
161
|
+
registering. `proofs/12-official-sections.ts` and `proofs/18-picker-pin.ts`
|
|
162
|
+
are the behavioral checks. The canonical pin and the upgrade procedure are in
|
|
163
|
+
[Versions and compatibility](docs/versions.md).
|
|
164
|
+
|
|
165
|
+
## Optional: the font plugin
|
|
166
|
+
|
|
167
|
+
[dsh-arbor-fonts](https://github.com/4fuu/dsh-arbor-fonts) is a separate DSH
|
|
168
|
+
client plugin that changes only the typography of the Arbor page. It sets
|
|
169
|
+
ChillDIN Gothic for display labels and ChillDIN for numerals, leaves body text
|
|
170
|
+
and code on DSH's fonts, and does nothing on ordinary pages. It is optional and
|
|
171
|
+
installs into the same DSH profile:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
dsh plugin --profile web add @4fu/dsh-arbor-fonts@2026.1110.0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The fonts are licensed under the SIL Open Font License 1.1; the plugin ships the
|
|
178
|
+
license text, per-file hashes and attribution.
|
|
179
|
+
|
|
180
|
+
## Limits worth knowing
|
|
181
|
+
|
|
182
|
+
- The judge is off: a model-based judge cannot run through the DSH backend yet.
|
|
183
|
+
- The 12 bundle packages are published together at one version; install and
|
|
184
|
+
upgrade the bundle as a unit, because its packages name each other exactly.
|
|
185
|
+
- `ovnfs` is optional. Without it arbor uses the plain-directory workspace, and a
|
|
186
|
+
window no longer gets a copy-on-write checkout.
|
|
187
|
+
- The web page's numbers and layout are read from arbor's journal and API; it is
|
|
188
|
+
not a generic DSH dashboard, and it deliberately shows only arbor sessions.
|
|
189
|
+
- The prompt bridge, the UI and the picker filter are pinned to DSH's internals,
|
|
190
|
+
not to a stable public API. See [Versions and compatibility](docs/versions.md).
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
- [Installing and inspecting dsh-arbor](docs/install-for-agents.md) — the
|
|
195
|
+
agent-facing install, proof, live-turn, web-profile and wiring guide.
|
|
196
|
+
- [Versions and compatibility](docs/versions.md) — the DSH and arbor pins, and
|
|
197
|
+
what breaks on another DSH version.
|
|
198
|
+
- [B0 findings](docs/b0-findings.md), [B3 findings](docs/b3-findings.md) and
|
|
199
|
+
[B3b projects and sandbox](docs/b3b-projects-and-sandbox.md) — historical
|
|
200
|
+
records of the integration's decisions, with the DSH source locations each
|
|
201
|
+
one rests on. They describe earlier stages; the install guide is the current
|
|
202
|
+
status.
|
|
203
|
+
- [Layout and proofs](#layout) below for a quick map of the packages.
|
|
204
|
+
|
|
205
|
+
## Layout
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
packages/
|
|
209
|
+
arbor-host/ one Arbor host per process: SQLite journal and notes,
|
|
210
|
+
settings, window lifecycle, workspace and environment
|
|
211
|
+
arbor-driver/ the routing AgentRegistry, AgentFactory, session agent,
|
|
212
|
+
inbox and stream mirrors, and the journal relay
|
|
213
|
+
arbor-llm/ the arbor AgentBackend over DSH's ctx.llm and ctx.tools
|
|
214
|
+
arbor-prompt/ <harness>, the <dsh> data block, and the section table
|
|
215
|
+
arbor-tools/ DSH tools as arbor external tools, same-name replacement
|
|
216
|
+
arbor-workspace/ arbor's workspace over the shared folder (ovnfs or dir)
|
|
217
|
+
arbor-env/ /env, with DSH's skills and MCP servers mounted read-only
|
|
218
|
+
arbor-bundle/ the installable patch that mounts the rows and UI
|
|
219
|
+
arbor-facts/ the read face behind the Arbor page and its Fetch routes
|
|
220
|
+
arbor-ui/ the Arbor page, session tabs, copy and composer gate
|
|
221
|
+
arbor-ui-workspace/
|
|
222
|
+
the per-preset hero project picker
|
|
223
|
+
arbor-preset/ the agent-preset row that puts Arbor in the roster
|
|
224
|
+
arbor-testkit/ the boot harness, memory journal and scripted model
|
|
225
|
+
patches/ overlay layers a proof applies above the shipped profile
|
|
226
|
+
proofs/ one runnable proof per integration point
|
|
227
|
+
docs/ install, versions, screenshots and historical findings
|
|
228
|
+
scripts/ the DSH pin check, the release build and the publish dry run
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT. See [LICENSE](LICENSE). The optional font plugin vendors fonts under the
|
|
234
|
+
SIL Open Font License 1.1; its repository carries the license text and
|
|
235
|
+
attribution.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local assistant attempt framing for arbor sessions.
|
|
3
|
+
*
|
|
4
|
+
* A compact mirror of `packages/core/agent-loop/src/assistant-stream.ts`
|
|
5
|
+
* (AssistantStreamAttempt): the same dsh-llm primitives
|
|
6
|
+
* (`AssistantStreamAccumulator`, `BlockAssembler`), the same frame order
|
|
7
|
+
* (start, chunk*, end), and the same settlement contract — the durable
|
|
8
|
+
* `assistant/message` or `assistant/attempt` append commits BEFORE the
|
|
9
|
+
* terminal frame is emitted.
|
|
10
|
+
*/
|
|
11
|
+
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent';
|
|
12
|
+
import { LlmAttemptId, type AssistantStreamRecord, type ContentBlock, type FinishReason, type ReplayEnvelope, type StreamChunk, type TokenUsage } from '@deepseek-ai/dsh-llm';
|
|
13
|
+
import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session';
|
|
14
|
+
/** Folds one model attempt into one compact stream plus ordered transient frames. */
|
|
15
|
+
export declare class ArborStreamAttempt {
|
|
16
|
+
private readonly accumulator;
|
|
17
|
+
private readonly assembler;
|
|
18
|
+
private index;
|
|
19
|
+
private terminal;
|
|
20
|
+
readonly attemptId: LlmAttemptId;
|
|
21
|
+
private readonly nextRevision;
|
|
22
|
+
readonly turn: number;
|
|
23
|
+
readonly step: number;
|
|
24
|
+
private readonly emit;
|
|
25
|
+
constructor(sessionId: SessionId, attempt: number, nextRevision: () => number, turn: number, step: number, emit: (frame: AssistantStreamFrame) => void);
|
|
26
|
+
get ended(): boolean;
|
|
27
|
+
/** Publish the opening marker before the first delivered chunk. */
|
|
28
|
+
start(): void;
|
|
29
|
+
/** Snapshot one chunk once, then feed durable compaction, assembly, and live publication. */
|
|
30
|
+
push(chunk: StreamChunk): void;
|
|
31
|
+
/** Publish terminal settlement after the matching durable event commits. */
|
|
32
|
+
settle(eventType: 'assistant/message' | 'assistant/attempt', append: () => SessionSeq): void;
|
|
33
|
+
/** Publish abandonment when no durable attempt event can be committed. */
|
|
34
|
+
abandon(): void;
|
|
35
|
+
/** Exact compact stream for the final durable event. */
|
|
36
|
+
get stream(): AssistantStreamRecord[];
|
|
37
|
+
/** Canonical completed-message blocks from the same chunks. */
|
|
38
|
+
blocks(): ContentBlock[];
|
|
39
|
+
/** Safe visible prefix when cancellation interrupts the attempt. */
|
|
40
|
+
interruptedBlocks(): ContentBlock[];
|
|
41
|
+
/** Latest adapter-reported usage in the stream. */
|
|
42
|
+
get usage(): TokenUsage | undefined;
|
|
43
|
+
/** Terminal reason, defaulting to stop when the stream omitted one. */
|
|
44
|
+
get finish(): FinishReason;
|
|
45
|
+
/** Replay state carried by the terminal finish record. */
|
|
46
|
+
get replayState(): ReplayEnvelope | undefined;
|
|
47
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local assistant attempt framing for arbor sessions.
|
|
3
|
+
*
|
|
4
|
+
* A compact mirror of `packages/core/agent-loop/src/assistant-stream.ts`
|
|
5
|
+
* (AssistantStreamAttempt): the same dsh-llm primitives
|
|
6
|
+
* (`AssistantStreamAccumulator`, `BlockAssembler`), the same frame order
|
|
7
|
+
* (start, chunk*, end), and the same settlement contract — the durable
|
|
8
|
+
* `assistant/message` or `assistant/attempt` append commits BEFORE the
|
|
9
|
+
* terminal frame is emitted.
|
|
10
|
+
*/
|
|
11
|
+
import { AssistantStreamAccumulator, BlockAssembler, LlmAttemptId, } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
/** Folds one model attempt into one compact stream plus ordered transient frames. */
|
|
13
|
+
export class ArborStreamAttempt {
|
|
14
|
+
accumulator = new AssistantStreamAccumulator();
|
|
15
|
+
assembler = new BlockAssembler();
|
|
16
|
+
index = 0;
|
|
17
|
+
terminal = false;
|
|
18
|
+
attemptId;
|
|
19
|
+
nextRevision;
|
|
20
|
+
turn;
|
|
21
|
+
step;
|
|
22
|
+
emit;
|
|
23
|
+
constructor(sessionId, attempt, nextRevision, turn, step, emit) {
|
|
24
|
+
this.nextRevision = nextRevision;
|
|
25
|
+
this.turn = turn;
|
|
26
|
+
this.step = step;
|
|
27
|
+
this.emit = emit;
|
|
28
|
+
this.attemptId = LlmAttemptId(`${sessionId}:${attempt}`);
|
|
29
|
+
}
|
|
30
|
+
get ended() {
|
|
31
|
+
return this.terminal;
|
|
32
|
+
}
|
|
33
|
+
/** Publish the opening marker before the first delivered chunk. */
|
|
34
|
+
start() {
|
|
35
|
+
this.emit({ type: 'start', attemptId: this.attemptId, revision: this.nextRevision(), turn: this.turn, step: this.step });
|
|
36
|
+
}
|
|
37
|
+
/** Snapshot one chunk once, then feed durable compaction, assembly, and live publication. */
|
|
38
|
+
push(chunk) {
|
|
39
|
+
const timed = this.accumulator.push({ time: Date.now(), chunk });
|
|
40
|
+
this.assembler.push(timed.chunk);
|
|
41
|
+
this.emit({
|
|
42
|
+
type: 'chunk',
|
|
43
|
+
attemptId: this.attemptId,
|
|
44
|
+
revision: this.nextRevision(),
|
|
45
|
+
index: this.index++,
|
|
46
|
+
time: timed.time,
|
|
47
|
+
chunk: timed.chunk,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Publish terminal settlement after the matching durable event commits. */
|
|
51
|
+
settle(eventType, append) {
|
|
52
|
+
let seq;
|
|
53
|
+
try {
|
|
54
|
+
seq = append();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.abandon();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
this.terminal = true;
|
|
61
|
+
this.emit({
|
|
62
|
+
type: 'end',
|
|
63
|
+
attemptId: this.attemptId,
|
|
64
|
+
revision: this.nextRevision(),
|
|
65
|
+
index: this.index,
|
|
66
|
+
outcome: { kind: 'committed', eventType, seq },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Publish abandonment when no durable attempt event can be committed. */
|
|
70
|
+
abandon() {
|
|
71
|
+
this.terminal = true;
|
|
72
|
+
this.emit({ type: 'end', attemptId: this.attemptId, revision: this.nextRevision(), index: this.index, outcome: { kind: 'abandoned' } });
|
|
73
|
+
}
|
|
74
|
+
/** Exact compact stream for the final durable event. */
|
|
75
|
+
get stream() {
|
|
76
|
+
return [...this.accumulator.snapshot()];
|
|
77
|
+
}
|
|
78
|
+
/** Canonical completed-message blocks from the same chunks. */
|
|
79
|
+
blocks() {
|
|
80
|
+
return this.assembler.blocks();
|
|
81
|
+
}
|
|
82
|
+
/** Safe visible prefix when cancellation interrupts the attempt. */
|
|
83
|
+
interruptedBlocks() {
|
|
84
|
+
return this.assembler.interruptedBlocks();
|
|
85
|
+
}
|
|
86
|
+
/** Latest adapter-reported usage in the stream. */
|
|
87
|
+
get usage() {
|
|
88
|
+
return this.assembler.usage;
|
|
89
|
+
}
|
|
90
|
+
/** Terminal reason, defaulting to stop when the stream omitted one. */
|
|
91
|
+
get finish() {
|
|
92
|
+
return this.assembler.finish;
|
|
93
|
+
}
|
|
94
|
+
/** Replay state carried by the terminal finish record. */
|
|
95
|
+
get replayState() {
|
|
96
|
+
return this.assembler.replayState;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/driver.d.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The arbor driver: the AgentFactory that runs arbor-preset sessions, and the
|
|
3
|
+
* Cordis plugin row that mounts it.
|
|
4
|
+
*
|
|
5
|
+
* One DSH user session is one conversation target of the process's single Arbor
|
|
6
|
+
* host (`@4fu/dsh-arbor-host`): the journal, the note store and the board are
|
|
7
|
+
* shared by every session, and each session is one source of that one host.
|
|
8
|
+
* Arbor's own model is what a session's windows follow — a window ends when its
|
|
9
|
+
* run ends and the session's next turn opens the next window of its chain, so
|
|
10
|
+
* several windows serve one session and none of them is the session. Per
|
|
11
|
+
* session the driver owns
|
|
12
|
+
*
|
|
13
|
+
* - the window, opened through `host.dispatch` on the session's first turn and
|
|
14
|
+
* taken back — not reopened — when the session already has a live one, which
|
|
15
|
+
* is what a restart's continuation is;
|
|
16
|
+
* - the bridge the session agent drives: `ensureWindow`, `modelCall`,
|
|
17
|
+
* `runToolCall` (arbor's tool path into `ctx.tools.execute`), `publish`,
|
|
18
|
+
* `finishRun`, `close`; and
|
|
19
|
+
* - the window's DSH agent registration, which keeps it out of the session
|
|
20
|
+
* list (`meta.origin: 'subagent'`, owned by the session's agent).
|
|
21
|
+
*
|
|
22
|
+
* `resume` loads the persisted session, replays onto its log what the arbor
|
|
23
|
+
* journal recorded and the log lost, and binds the session back to the window
|
|
24
|
+
* the journal says is its own — the window the host continued at boot.
|
|
25
|
+
*
|
|
26
|
+
* The creation lifecycle mirrors `AgentLoop.createAgent`
|
|
27
|
+
* (packages/core/agent-loop/src/index.ts): prepare the session, take
|
|
28
|
+
* persistence ownership when a backend is mounted, run setup while the agent
|
|
29
|
+
* is unpublished, then enter and announce the session and the agent through
|
|
30
|
+
* the same registries the official loop uses.
|
|
31
|
+
*
|
|
32
|
+
* B1 scope, per the plan: compaction, goals, agentTeams, retry and fork are
|
|
33
|
+
* off for arbor sessions.
|
|
34
|
+
*/
|
|
35
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
36
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
37
|
+
import z from '@deepseek-ai/schemastery';
|
|
38
|
+
import type { Agent, AgentFactory, AgentHandle, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent';
|
|
39
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
40
|
+
import { type ArborHostService } from '@4fu/dsh-arbor-host';
|
|
41
|
+
import type { WindowRef } from '@4fu/arbor-interfaces';
|
|
42
|
+
/** Plugin config: the arbor settings live in the host row, so there is nothing here. */
|
|
43
|
+
export type Config = Record<string, never>;
|
|
44
|
+
/** What the driver exposes about one session's arbor world. */
|
|
45
|
+
export interface SessionArborLike {
|
|
46
|
+
/** The window this session's work runs in, once it has one. */
|
|
47
|
+
readonly windowRef: WindowRef | undefined;
|
|
48
|
+
/** The agent registered for this session's window, once one exists. */
|
|
49
|
+
readonly windowAgentOf: Agent | undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The arbor agent factory and Cordis plugin. */
|
|
52
|
+
export declare class ArborDriver extends Service implements AgentFactory {
|
|
53
|
+
static inject: string[];
|
|
54
|
+
static Config: z<Config, Config>;
|
|
55
|
+
readonly config: Config;
|
|
56
|
+
/** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
|
|
57
|
+
private readonly runtime;
|
|
58
|
+
private readonly sessions;
|
|
59
|
+
/**
|
|
60
|
+
* Sessions the embedded AgentLoop is running, with the handle its creation
|
|
61
|
+
* returned. The driver created every one of them — the registry routes every
|
|
62
|
+
* session through this factory — so it holds the only thing that can end one:
|
|
63
|
+
* that is what lets a preset selection re-route a session that has not started
|
|
64
|
+
* a turn. An ordinary session is never touched unless its user picks arbor.
|
|
65
|
+
*/
|
|
66
|
+
private readonly embedded;
|
|
67
|
+
private modelProjectionHandle;
|
|
68
|
+
private inboxProjectionHandle;
|
|
69
|
+
constructor(ctx: Context, config?: Config);
|
|
70
|
+
protected [Service.init](): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Re-route one session from the embedded AgentLoop to arbor.
|
|
73
|
+
*
|
|
74
|
+
* The loop's agent is ended with the handle its creation returned — which
|
|
75
|
+
* unregisters the agent and detaches the session from the store, leaving the
|
|
76
|
+
* durable log exactly as it stands — and the session is then opened again
|
|
77
|
+
* under arbor, the way a restart opens it. The session keeps its identity, its
|
|
78
|
+
* log and every event it already committed, so the composer that is bound to
|
|
79
|
+
* it keeps working; what changes is which driver its next turn runs.
|
|
80
|
+
*/
|
|
81
|
+
private rerouteToArbor;
|
|
82
|
+
/**
|
|
83
|
+
* Record a directory an arbor session named as an external pointer for that
|
|
84
|
+
* session, when the user had added the directory through DSH's own
|
|
85
|
+
* add-workspace action.
|
|
86
|
+
*
|
|
87
|
+
* The host answers whether the directory was added, because that is a fact
|
|
88
|
+
* about the project list rather than about this session; a directory that was
|
|
89
|
+
* never added — a cwd the session inherited — is left alone, which is the
|
|
90
|
+
* mistake the earlier resolve-time recording made.
|
|
91
|
+
*/
|
|
92
|
+
private noteExternalProject;
|
|
93
|
+
/**
|
|
94
|
+
* Re-route one session from arbor back to the embedded AgentLoop.
|
|
95
|
+
*
|
|
96
|
+
* The mirror of {@link rerouteToArbor}, and it exists for one case only: a
|
|
97
|
+
* session that SELECTED arbor while it was blank can select something else
|
|
98
|
+
* again while it is still blank, and the label and the driver under it must
|
|
99
|
+
* say the same thing. That is the SDK or remote `agent-preset/select` path —
|
|
100
|
+
* every surface that knows what the user clicked creates the session under
|
|
101
|
+
* the picked preset instead (the hero mint, the adopt at create), so nothing
|
|
102
|
+
* in the web profile switches a blank in place. The arbor agent is ended with
|
|
103
|
+
* the handle this driver holds, the window keeps running out arbor's own idle
|
|
104
|
+
* policy, and the session is opened again on the embedded loop with the same
|
|
105
|
+
* id, the same log and the same cwd it was created with.
|
|
106
|
+
*
|
|
107
|
+
* **A session created under arbor is never moved off it here.** Its creation
|
|
108
|
+
* header says `agentPreset: 'arbor'`, which is a frozen creation fact
|
|
109
|
+
* (`@deepseek-ai/dsh-agent-preset-registry`, `lib/types/session.js`), and the
|
|
110
|
+
* one selection that can still arrive on it is DSH's own: the session
|
|
111
|
+
* controller re-selects the resolved deployment default whenever a blank is
|
|
112
|
+
* reused with no preset named — `reuseLive` calls `presets.select` with
|
|
113
|
+
* `composition.agentPreset` and `reuseSetup` appends the event itself
|
|
114
|
+
* (`@deepseek-ai/dsh-api-session-controller`, `lib/index.js`). The log cannot
|
|
115
|
+
* tell that re-select from a user's pick, because `presets.select` is the
|
|
116
|
+
* single writer of `agent-preset/selected` (`lib/index.js`), so the
|
|
117
|
+
* attribution has to come from the header instead: a session created as arbor
|
|
118
|
+
* keeps arbor, and its label is put back through the registry's own select so
|
|
119
|
+
* the projection and the driver agree ({@link reassertArbor}). The client side
|
|
120
|
+
* of the same guarantee is that the restore names the recorded preset, so the
|
|
121
|
+
* re-select is never written in the first place — this gate is what stands
|
|
122
|
+
* behind it.
|
|
123
|
+
*
|
|
124
|
+
* A session that has started a turn keeps the driver it began with: DSH refuses
|
|
125
|
+
* that selection before it is committed, so this is the same defensive guard the
|
|
126
|
+
* arbor direction carries, and the alternative — silently keeping the old driver
|
|
127
|
+
* — is the one outcome the owner ruled out.
|
|
128
|
+
*/
|
|
129
|
+
private rerouteToEmbedded;
|
|
130
|
+
/**
|
|
131
|
+
* Put a session's label back on arbor, through the registry's own select.
|
|
132
|
+
*
|
|
133
|
+
* The selection being refused has already been appended to the log, so the
|
|
134
|
+
* session's `agentPreset` projection now names another preset while this
|
|
135
|
+
* driver still runs arbor — a label that says one thing while the driver does
|
|
136
|
+
* another, which is the untruth the gate exists to prevent. `presets.select`
|
|
137
|
+
* is the single writer of that event (`@deepseek-ai/dsh-agent-preset-registry`,
|
|
138
|
+
* `lib/index.js`), and it appends only when the projection does not already
|
|
139
|
+
* name the preset, so this writes one honest event and nothing else: the
|
|
140
|
+
* registry's `bind` is a no-op when the agent's context already holds the
|
|
141
|
+
* preset's generation. The event reaches this driver's own observer, which
|
|
142
|
+
* finds the session already running arbor and does nothing.
|
|
143
|
+
*/
|
|
144
|
+
private reassertArbor;
|
|
145
|
+
/** The driver's own DSH context, for the per-session arbor worlds. */
|
|
146
|
+
get driverContext(): Context;
|
|
147
|
+
/** The one Arbor host of this process. */
|
|
148
|
+
get hostService(): ArborHostService;
|
|
149
|
+
/** The per-session arbor world, for tests and diagnostics. */
|
|
150
|
+
sessionArbor(sessionId: SessionId): SessionArborLike | undefined;
|
|
151
|
+
/** The agent registered for a session's arbor window, once one exists. */
|
|
152
|
+
windowAgentOf(sessionId: SessionId): Agent | undefined;
|
|
153
|
+
/** The system prompt the session's arbor window rendered, once it opened. */
|
|
154
|
+
systemPromptOf(sessionId: SessionId): string | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* Whether a persisted session runs under arbor.
|
|
157
|
+
*
|
|
158
|
+
* The creation header names the preset a session STARTED with and is frozen
|
|
159
|
+
* because it is a creation fact; a session may still select another while it
|
|
160
|
+
* is blank, and DSH's own reconstruction rule for exactly this reason reads
|
|
161
|
+
* the `agentPreset` projection, "never the header alone"
|
|
162
|
+
* (`@deepseek-ai/dsh-agent-preset-registry`, `lib/types/session.js`). So the
|
|
163
|
+
* log's `agent-preset/selected` events are folded over the header's value:
|
|
164
|
+
* that is what makes a re-routed session come back as arbor after a restart
|
|
165
|
+
* instead of silently resuming on the embedded loop.
|
|
166
|
+
*/
|
|
167
|
+
ownsSession(sessionId: SessionId): Promise<boolean>;
|
|
168
|
+
createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;
|
|
169
|
+
/** Create the agent of a session that runs arbor. */
|
|
170
|
+
private createArborAgent;
|
|
171
|
+
/**
|
|
172
|
+
* Create an agent for a session the embedded AgentLoop will run.
|
|
173
|
+
*
|
|
174
|
+
* The embedded factory is delegated to exactly the way the base
|
|
175
|
+
* `AgentRegistry` delegates, so an ordinary session's agent, its ownership and
|
|
176
|
+
* its teardown are the official loop's own. The one difference is that this
|
|
177
|
+
* driver keeps the handle: a session whose user later picks arbor is re-routed
|
|
178
|
+
* by ending the loop's agent and opening the session's arbor window, and
|
|
179
|
+
* ending an agent needs the handle its creation returned.
|
|
180
|
+
*/
|
|
181
|
+
private createEmbedded;
|
|
182
|
+
/** Resume an agent for a session the embedded AgentLoop will run. */
|
|
183
|
+
private resumeEmbedded;
|
|
184
|
+
/**
|
|
185
|
+
* Resume a persisted arbor session.
|
|
186
|
+
*
|
|
187
|
+
* The session's log is loaded the way the official loop loads it, with two
|
|
188
|
+
* arbor-specific steps around it: the window chain the journal holds is read
|
|
189
|
+
* back and the `tool/call` events the log lost are replayed onto it, so DSH's
|
|
190
|
+
* own `interruptedTurnClosers` closes them as outcome-unknown rather than
|
|
191
|
+
* not-started; and the session is bound to the window the host continued at
|
|
192
|
+
* boot, so its next turn drives that window instead of opening a new chain.
|
|
193
|
+
*/
|
|
194
|
+
resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;
|
|
195
|
+
/**
|
|
196
|
+
* The cwd a persisted session was created with, read without taking its write
|
|
197
|
+
* ownership: the resume path needs it to resolve the session's cwd before it
|
|
198
|
+
* opens the log for writing.
|
|
199
|
+
*/
|
|
200
|
+
private storedCwd;
|
|
201
|
+
/** Resume a persisted session that runs arbor. */
|
|
202
|
+
private resumeArbor;
|
|
203
|
+
/** Build the session agent for one prepared session, create or resume. */
|
|
204
|
+
private agentFor;
|
|
205
|
+
/** Take a fresh session's write ownership when persistence is mounted. */
|
|
206
|
+
private createStoredSession;
|
|
207
|
+
/** Durably store the session events appended since the last stored cursor. */
|
|
208
|
+
private appendUnstoredSuffix;
|
|
209
|
+
/** Enter and announce the session and the agent, exactly like AgentLoop does. */
|
|
210
|
+
private publish;
|
|
211
|
+
}
|
|
212
|
+
export default ArborDriver;
|