@4fu/arbor-interfaces 2026.1106.0 → 2026.1107.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.md CHANGED
@@ -27,9 +27,11 @@ everything that takes judgment, such as git, merges and conflicts, to the agent.
27
27
  guarantees what an agent cannot guarantee for itself - the record, isolation,
28
28
  exactly-once effects, the checks, dispatch. Everything that takes judgment,
29
29
  such as git, merges and conflicts, is the agent's.
30
- - **A directory per window.** Each window gets its own directory, and a
31
- continuation keeps working in its predecessor's directory in place. It is not
32
- bound to a repository; the agent clones and pushes what it needs.
30
+ - **A shared workspace with per-window checkouts.** The default workspace is
31
+ ovnfs: one shared folder every window reads, and a window opens its own
32
+ copy-on-write checkout of the project it needs, works there and merges back
33
+ with `apply`. It falls back to a plain directory per window when ovnfs is not
34
+ installed. It is not a sandbox: ovnfs isolates files, not processes.
33
35
  - **A self-changing environment.** The model adds and changes skills, MCP
34
36
  servers and resident services itself; a `reload` validates the change and
35
37
  broadcasts it to every live window as a delta of names and descriptions.
@@ -54,7 +56,9 @@ npm install @4fu/arbor-core @4fu/arbor-journal-sqlite @4fu/arbor-notes-sqlite \
54
56
  ```
55
57
 
56
58
  `@4fu/arbor-plugin-http` is optional; it is the example plugin below.
57
- `@4fu/arbor-segmenter-jieba` is an optional peer of the journal.
59
+ `@4fu/arbor-segmenter-jieba` is an optional peer of the lexical package, and
60
+ `@4fu/arbor-search-zvec` an optional peer of the journal: absent either one, the
61
+ journal searches lexically and nothing else changes.
58
62
 
59
63
  ## A minimal host
60
64
 
@@ -118,7 +122,10 @@ window gets, and how a continuation keeps working in it, are in
118
122
  | [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
119
123
  | [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
120
124
  | [`@4fu/arbor-segmenter-jieba`](packages/segmenter-jieba) | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
121
- | [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
125
+ | [`@4fu/arbor-lexical`](packages/lexical) | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
126
+ | [`@4fu/arbor-search-zvec`](packages/search-zvec) | the optional semantic channel: event text embedded and searched by meaning with zvec |
127
+ | [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the fallback `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
128
+ | [`@4fu/arbor-workspace-ovnfs`](packages/workspace-ovnfs) | the default `Workspace` when ovnfs is installed: one shared folder, per-window copy-on-write checkouts the model opens and applies back |
122
129
  | [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
123
130
  | [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
124
131
  | [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
@@ -151,7 +158,7 @@ deployment can see what it would be taking on before it adopts one.
151
158
  | [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
152
159
  | [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
153
160
  | [docs/memory.md](docs/memory.md) | notes, the active set, anchors, links |
154
- | [docs/workspaces.md](docs/workspaces.md) | per-window directories: isolation, continuity, what the agent does |
161
+ | [docs/workspaces.md](docs/workspaces.md) | the shared folder and per-window checkouts: isolation, continuity, what the agent does |
155
162
  | [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
156
163
  | [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
157
164
  | [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
@@ -168,6 +175,7 @@ tarball would contain ([docs/release.md](docs/release.md)).
168
175
  | `packages/errors` | the error shape every refusal uses: the rule and the next action |
169
176
  | `packages/journal-sqlite` | the default `Journal` and `BlobStore` on SQLite |
170
177
  | `packages/segmenter-jieba` | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
178
+ | `packages/lexical` | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
171
179
  | `packages/notes-sqlite` | the default `NoteStore` on SQLite, beside the journal |
172
180
  | `packages/judge-jev` | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
173
181
  | `packages/workspace-dir` | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place, and the lookup of another window's directory it contributes to the catalog |
package/agent.d.ts CHANGED
@@ -1,17 +1,29 @@
1
1
  /**
2
2
  * The `AgentBackend` interface: run one window - send the prompt and the tools
3
- * to a model, stream tool calls, accept inputs inserted once a turn's tool
4
- * calls finish, report token usage, list the models it can run and log in to
5
- * their providers (docs/architecture.md#interfaces).
3
+ * to a model, stream tool calls and what the model is writing, accept inputs
4
+ * inserted once a turn's tool calls finish, report token usage, list the models
5
+ * it can run and log in to their providers (docs/architecture.md#interfaces).
6
6
  *
7
7
  * arbor uses pi-agent-core's `Agent` with its own system prompt: pi's default
8
8
  * coding prompt, context files and skills never reach the model
9
9
  * (docs/prompting.md#no-built-in-pi-prompt).
10
10
  */
11
11
  import type { Block } from "./blocks.ts";
12
+ import type { ExternalTool } from "./tools.ts";
12
13
  import type { TaskRef, Ref, WindowRef } from "./refs.ts";
13
14
  import type { Handoff, TokenUsage, WindowState } from "./windows.ts";
14
15
  import type { RateLimitState } from "./scheduler.ts";
16
+ /**
17
+ * One JSON Schema object, carried as it was declared.
18
+ *
19
+ * A tool declared from outside the core - a plugin's own, or a harness's
20
+ * through a backend - hands its parameters over in the JSON Schema that
21
+ * harness already speaks, and that schema is what the model reads, verbatim
22
+ * (docs/tools.md#external-tools). Deriving one from a summary would drop what
23
+ * the schema says and what the summary cannot: `enum`, `items`, `oneOf`,
24
+ * `additionalProperties`, a nested object's own properties.
25
+ */
26
+ export type JSONSchema = Record<string, unknown>;
15
27
  /** One model the backend can run. Only models that take images are offered. */
16
28
  export interface ModelInfo {
17
29
  id: string;
@@ -77,9 +89,47 @@ export interface ContinuationInfo {
77
89
  /** Set after the hard limit: the window was reset mid-task. */
78
90
  midTaskReset?: boolean;
79
91
  }
80
- /** What the host inserts into a running window at a tool-call boundary. */
92
+ /**
93
+ * What the host inserts into a running window at a tool-call boundary.
94
+ *
95
+ * ## What each kind expects from a backend
96
+ *
97
+ * Every delivery is one input block, and the host says when the model must
98
+ * read it by the call it hands the delivery to: `WindowSession.steer` for one
99
+ * that goes in at the next step boundary, `WindowSession.followUp` for one that
100
+ * becomes the input of the next turn. The rule is the same for every kind - the
101
+ * insertion rules decide, not the kind
102
+ * (docs/dispatch.md#insertion-timing) - with two things a backend can rely on:
103
+ *
104
+ * - **What a window can call is never held back.** A conversation update, an
105
+ * environment delta, a prompt delta and a tools delta always come through
106
+ * `steer`, in the protection period and after it, because they change what
107
+ * the window would otherwise do next.
108
+ * - **A reminder is the kind the host times itself.** While the window runs it
109
+ * is steered in at the next boundary; once the run has ended it is queued as
110
+ * a follow-up, so it is the input of the window's next turn.
111
+ *
112
+ * A backend that is not pi maps the two calls onto whatever its own loop
113
+ * offers - DeepSeek Harness's `agent.inject` for a step boundary and its
114
+ * steering for the next turn - and the rest of the contract is the same: the
115
+ * block is one user-role input, in order, exactly once, and the turn the
116
+ * window is in is never ended by it.
117
+ */
81
118
  export interface Delivery {
82
119
  kind: "inserted_task" | "conversation_update" | "notice" | "broadcast" | "window_message" | "environment_delta"
120
+ /**
121
+ * A plugin's prompt section changed since this window's prefix was built:
122
+ * the lines that changed, and nothing that did not
123
+ * (docs/prompting.md#plugin-sections-that-change).
124
+ */
125
+ | "prompt_delta"
126
+ /**
127
+ * The direct tools declared from outside the core changed while this window
128
+ * runs: a harness registered one, or a plugin's set moved. The delta names
129
+ * what was added and what was removed, and the window may call the new ones
130
+ * from its next tool call on (docs/tools.md#external-tools-that-change).
131
+ */
132
+ | "tools_delta"
83
133
  /** A host usage reminder, appended to an input (docs/windows.md#usage-reminders). */
84
134
  | "reminder";
85
135
  /** The window it comes from, for a notice, a broadcast or a window message. */
@@ -92,23 +142,106 @@ export interface Delivery {
92
142
  export interface WindowSession {
93
143
  readonly window: WindowRef;
94
144
  readonly state: WindowState;
95
- /** Insert at the next tool-call boundary, through the backend's steering. */
145
+ /**
146
+ * Insert at the next tool-call boundary, through the backend's steering.
147
+ *
148
+ * A **step boundary** is the moment every tool call of the current turn has
149
+ * finished and the next model request has not gone out yet. The delivery
150
+ * becomes one user-role input the model reads with that request. It is never
151
+ * a system message in the middle of a window - some providers fold those into
152
+ * the leading system prompt and that breaks the cached prefix
153
+ * (docs/prompting.md#no-built-in-pi-prompt) - and it never rewrites the
154
+ * window's prefix.
155
+ *
156
+ * What a backend must do:
157
+ *
158
+ * - deliver the block in the order the host handed its deliveries over, and
159
+ * deliver each one exactly once;
160
+ * - keep the turn going: the model is not stopped, not asked to confirm, and
161
+ * not told the input is special - it is one more input;
162
+ * - start a turn when the window is idle, because that is what a related
163
+ * input wants (docs/dispatch.md#insertion-timing); the host only steers
164
+ * into a window it has not ended and has not paused, so a delivery never
165
+ * arrives for one of those;
166
+ * - resolve once the delivery is queued, not once the model has read it: the
167
+ * host never waits for the model, and a slow model is not a slow steer.
168
+ *
169
+ * A window that has ended refuses in the core's error shape, rule
170
+ * `window.ended`; the host never steers into one.
171
+ */
96
172
  steer(delivery: Delivery): Promise<void>;
97
- /** Queue for after the run ends. */
173
+ /**
174
+ * Queue for after the run ends: the delivery becomes the input of the
175
+ * window's next turn.
176
+ *
177
+ * What a backend must do:
178
+ *
179
+ * - hold it out of the turn the window is in - the model must not read it
180
+ * mid-turn, which is what separates this call from `steer`;
181
+ * - deliver it as the input of the next turn, in order and exactly once, when
182
+ * the window runs again;
183
+ * - keep it queued while the window sits idle, and drop it when the window
184
+ * ends, because an ended window is never reopened
185
+ * (docs/windows.md#lifecycle).
186
+ *
187
+ * The host uses this only for a delivery that belongs after the run: the
188
+ * reminder of a window whose run has already ended, and whatever a
189
+ * `Scheduler` places there (docs/dispatch.md#insertion-timing).
190
+ */
98
191
  followUp(delivery: Delivery): Promise<void>;
192
+ /**
193
+ * The direct tools this window may call changed: the host registered what a
194
+ * backend or a plugin declared since this window opened
195
+ * (docs/tools.md#external-tools-that-change).
196
+ *
197
+ * A backend that can change a running window's declared tools replaces them
198
+ * here, so a tool that appeared is callable in this window and one that was
199
+ * removed is not. A backend that cannot leaves the declaration it opened
200
+ * with: the window still learns the change from the `tools_delta` it is
201
+ * steered, and a call still runs through the host's catalog, which holds the
202
+ * current set. Optional; the host asks only of a session that has it.
203
+ */
204
+ refreshTools?(tools: readonly DirectToolSpec[]): Promise<void> | void;
99
205
  /** End the run. With a handoff the host opens a continuation. */
100
206
  end(handoff?: Handoff, reason?: WindowEndReason): Promise<void>;
101
207
  /** Pause insertion without ending the window. */
102
208
  pause(): Promise<void>;
103
209
  resume(): Promise<void>;
104
210
  usage(): TokenUsage;
105
- /** Tool calls, results and appends as they happen. */
211
+ /** Tool calls, results and what the model is writing, as they happen. */
106
212
  events(): AsyncIterable<WindowEvent>;
107
213
  /** Resolves when the window ends, with why. */
108
214
  outcome(): Promise<WindowOutcome>;
109
215
  }
216
+ /**
217
+ * What a window is writing while it runs, one delta at a time
218
+ * (docs/windows.md#streaming).
219
+ *
220
+ * Streams are best-effort and ephemeral: they are not journaled, and the
221
+ * journal's durable events stay the only record. A backend that cannot report
222
+ * deltas never emits them, and a consumer that misses one keeps going - the
223
+ * next delta carries what stands so far.
224
+ */
225
+ export type StreamEvent =
226
+ /** Assistant text, as the model writes it. */
227
+ {
228
+ kind: "text_delta";
229
+ delta: string;
230
+ }
231
+ /**
232
+ * One tool call's arguments, as the model writes them. `call.args` is what
233
+ * has been decoded so far and is best-effort: a call still being written is
234
+ * not valid JSON yet, so a string argument holds the characters written so
235
+ * far - which is what makes `send`'s content visible while the model is
236
+ * still writing it (docs/tools.md#direct-tools). The durable `tool_call`
237
+ * event carries the complete arguments, once.
238
+ */
239
+ | {
240
+ kind: "tool_call_delta";
241
+ call: ToolCall;
242
+ };
110
243
  /** What a backend streams while a window runs. */
111
- export type WindowEvent = {
244
+ export type WindowEvent = StreamEvent | {
112
245
  kind: "tool_call";
113
246
  call: ToolCall;
114
247
  } | {
@@ -153,6 +286,20 @@ export interface AgentBackend {
153
286
  * reports nothing is never limited.
154
287
  */
155
288
  rateLimits?(): Promise<RateLimitState> | RateLimitState;
289
+ /**
290
+ * Direct tools the backend injects from outside arbor: the tools an outside
291
+ * harness registered, which a host embedded in that harness hands in here
292
+ * (docs/tools.md#external-tools). They are declared to the model and called
293
+ * in the turn like any other direct tool.
294
+ *
295
+ * The set is refreshable: the host reads it again before every window opens
296
+ * and at every step boundary, so a tool the harness registers - or drops -
297
+ * while the host runs reaches the model without a restart
298
+ * (docs/tools.md#external-tools-that-change). A backend that wants the change
299
+ * to land the moment it happens rather than at the next boundary calls
300
+ * `Arbor.refreshExternalTools` from its own signal.
301
+ */
302
+ externalTools?(): readonly ExternalTool[] | Promise<readonly ExternalTool[]>;
156
303
  /** Open one window and start its first turn. */
157
304
  open(request: OpenWindowRequest): Promise<WindowSession>;
158
305
  }
@@ -161,7 +308,23 @@ export interface DirectToolSpec {
161
308
  name: string;
162
309
  /** What it does and when to use it; side effects. 1 to 4 sentences. */
163
310
  description: string;
311
+ /** The parameters in arbor's own declaration shape. */
164
312
  parameters: Record<string, ParameterSpec>;
313
+ /**
314
+ * The parameters as the JSON Schema an outside harness declared them, carried
315
+ * verbatim: `enum`, `items`, `oneOf`, `additionalProperties`, a nested
316
+ * object's own properties, all of it. What the model reads when the tool has
317
+ * one, and what the `<tools>` line and `tool_search` show
318
+ * (docs/tools.md#external-tools). Arbor's own tools declare none: their
319
+ * `parameters` are the declaration.
320
+ */
321
+ schema?: JSONSchema;
322
+ /**
323
+ * Who declares it, when it is not the core's own: a plugin's name, or a
324
+ * backend's. The tools list says where such a tool comes from, so the model
325
+ * knows whose it is (docs/tools.md#external-tools).
326
+ */
327
+ source?: string;
165
328
  }
166
329
  /** Called from a script in `code_exec` (docs/tools.md#modules). */
167
330
  export interface ModuleSpec {
@@ -193,6 +356,12 @@ export interface ToolResult {
193
356
  ok: boolean;
194
357
  /** Stable fields, bounded, in the core's error shape when it failed. */
195
358
  content: string;
359
+ /**
360
+ * The tool that was called, as the prompt names it. A refusal carries it so
361
+ * the error line starts with the tool name; the catalog fills it on the way
362
+ * out, so a group that leaves it out still names the call.
363
+ */
364
+ tool?: string;
196
365
  error?: {
197
366
  rule: string;
198
367
  message: string;
package/agent.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The `AgentBackend` interface: run one window - send the prompt and the tools
3
- * to a model, stream tool calls, accept inputs inserted once a turn's tool
4
- * calls finish, report token usage, list the models it can run and log in to
5
- * their providers (docs/architecture.md#interfaces).
3
+ * to a model, stream tool calls and what the model is writing, accept inputs
4
+ * inserted once a turn's tool calls finish, report token usage, list the models
5
+ * it can run and log in to their providers (docs/architecture.md#interfaces).
6
6
  *
7
7
  * arbor uses pi-agent-core's `Agent` with its own system prompt: pi's default
8
8
  * coding prompt, context files and skills never reach the model
package/blocks.d.ts CHANGED
@@ -23,6 +23,12 @@ export interface BlockFieldSpec {
23
23
  name: string;
24
24
  type: "string" | "strings" | "number" | "boolean" | "ref" | "refs" | "blocks";
25
25
  required?: boolean;
26
+ /**
27
+ * True when the field names the block's own XML tag rather than content: the
28
+ * renderer writes it as the tag and never as a line of its own
29
+ * (docs/prompting.md#blocks-and-the-renderer).
30
+ */
31
+ tag?: boolean;
26
32
  }
27
33
  /**
28
34
  * A plugin's registration of a block kind: its fields, the tag the renderer
@@ -41,6 +47,12 @@ export interface BlockKindSpec {
41
47
  extends?: string;
42
48
  /** XML tag the renderer wraps one block in; omitted for an untagged block. */
43
49
  tag?: string;
50
+ /**
51
+ * The field whose value is this block's XML tag, for a kind whose tag is
52
+ * chosen per instance - a plugin prompt section's delta, which renders in the
53
+ * tag the plugin named. Omitted for a kind whose tag is fixed.
54
+ */
55
+ tagField?: string;
44
56
  fields: readonly BlockFieldSpec[];
45
57
  /** Fixed lines written before the fields, by key into the text table. */
46
58
  lines?: readonly string[];
package/events.d.ts CHANGED
@@ -108,7 +108,11 @@ export interface SearchHit {
108
108
  kind: EventKind;
109
109
  time: number;
110
110
  match: MatchKind;
111
- /** Relevance in 0..1 within its index; higher is better. */
111
+ /**
112
+ * Relevance in 0..1 across the channels that found the hit; higher is better.
113
+ * Fused with reciprocal rank fusion, so a hit two channels found outranks one
114
+ * only one did.
115
+ */
112
116
  score: number;
113
117
  /** The event's opening words, for a one-line result. */
114
118
  summary: string;
@@ -151,7 +155,7 @@ export interface TraceNode {
151
155
  /** A target a window is behind on. */
152
156
  export interface TargetGap {
153
157
  target: Target;
154
- /** The newest event on the target. */
158
+ /** The newest event on the target another window wrote after this one opened. */
155
159
  latest: number;
156
160
  /** The newest event on the target the window has seen. */
157
161
  watermark: number;
package/hooks.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * built from these.
9
9
  */
10
10
  import type { ArborErrorInfo } from "@4fu/arbor-errors";
11
- import type { Delivery, RenderedPrompt, ToolCall, ToolResult } from "./agent.ts";
11
+ import type { Delivery, RenderedPrompt, StreamEvent, ToolCall, ToolResult } from "./agent.ts";
12
12
  import type { Block } from "./blocks.ts";
13
13
  import type { Event, EventInput } from "./events.ts";
14
14
  import type { Effect } from "./effects.ts";
@@ -27,6 +27,11 @@ export type HookOutcome<T> = {
27
27
  allow: false;
28
28
  error: ArborErrorInfo;
29
29
  };
30
+ /**
31
+ * What an after hook answers with: the outcome of what it observed, or nothing
32
+ * at all. An after hook that observes only answers with nothing.
33
+ */
34
+ export type AfterHookOutcome<T> = HookOutcome<T> | void;
30
35
  export interface AppendContext {
31
36
  input: EventInput;
32
37
  event?: Event;
@@ -67,12 +72,28 @@ export interface WindowContinueContext {
67
72
  export interface ModelRequestContext {
68
73
  window: WindowRef;
69
74
  prompt: RenderedPrompt;
75
+ /**
76
+ * The blocks the window's first input is rendered from, and what a hook
77
+ * changes is what the window opens with: the host renders what comes back.
78
+ * The hook runs once per window, before the first turn
79
+ * (docs/windows.md#prompt-layout).
80
+ */
70
81
  inputs: readonly Block[];
71
82
  }
72
83
  export interface DeliveryContext {
73
84
  window: WindowRef;
74
85
  delivery: Delivery;
75
86
  }
87
+ /**
88
+ * One delta of a window's live output, as the model writes it. What a window
89
+ * says to the outside is the `content` argument of the `send` direct tool, so a
90
+ * surface that shows a window's output live reads `tool_call_delta`
91
+ * (docs/windows.md#streaming).
92
+ */
93
+ export interface WindowStreamContext {
94
+ window: WindowRef;
95
+ event: StreamEvent;
96
+ }
76
97
  export interface ToolCallContext {
77
98
  window: WindowRef;
78
99
  call: ToolCall;
@@ -127,8 +148,21 @@ export interface Hooks {
127
148
  windowContinue?(context: WindowContinueContext): Promise<HookOutcome<WindowContinueContext>> | HookOutcome<WindowContinueContext>;
128
149
  beforeModelRequest?(context: ModelRequestContext): Promise<HookOutcome<ModelRequestContext>> | HookOutcome<ModelRequestContext>;
129
150
  beforeDelivery?(context: DeliveryContext): Promise<HookOutcome<DeliveryContext>> | HookOutcome<DeliveryContext>;
151
+ /**
152
+ * One delta of a window's live output. Observation only, and best-effort: it
153
+ * can neither change nor refuse anything, it is not waited for, and a hook
154
+ * that throws or is slow loses its deltas rather than the window
155
+ * (docs/windows.md#streaming).
156
+ */
157
+ windowStream?(context: WindowStreamContext): Promise<void> | void;
130
158
  beforeToolCall?(context: ToolCallContext): Promise<HookOutcome<ToolCallContext>> | HookOutcome<ToolCallContext>;
131
- afterToolCall?(context: ToolCallContext): Promise<void> | void;
159
+ /**
160
+ * After one call has run. The call is already made, so a refusal cannot undo
161
+ * it: it stops the hooks after it and leaves the result as it stands. What it
162
+ * answers with is used - a hook that returns a different `result` replaces
163
+ * what is recorded and what the model reads (docs/architecture.md#hooks).
164
+ */
165
+ afterToolCall?(context: ToolCallContext): Promise<AfterHookOutcome<ToolCallContext>> | AfterHookOutcome<ToolCallContext>;
132
166
  beforeEffect?(context: EffectContext): Promise<EffectOutcome> | EffectOutcome;
133
167
  beforeNoteWrite?(context: NoteWriteContext): Promise<HookOutcome<NoteWriteContext>> | HookOutcome<NoteWriteContext>;
134
168
  afterNoteWrite?(context: NoteWriteContext): Promise<void> | void;
package/index.d.ts CHANGED
@@ -11,19 +11,19 @@ export type { BlockValue, Block, BlockKind, BlockFieldSpec, BlockKindSpec, TextT
11
11
  /** Page bounds every read reports (docs/prompting.md). */
12
12
  export { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT } from "./refs.ts";
13
13
  export type { EventInput, Link, LinkInput, Redaction, Event, ListOptions, Page, EventPage, AroundOptions, SearchOptions, SearchHit, SearchPage, LinksOptions, RelatedItem, LinkPage, TraceOptions, TraceNode, TargetGap, JournalError, } from "./events.ts";
14
- export type { BlobInput, BlobContent, BlobInfo, BlobStore, Segmenter, Journal, } from "./journal.ts";
15
- export type { ModelInfo, LoginState, LoginRequest, LoginResult, RenderedPrompt, OpenWindowRequest, WindowLimits, ContinuationInfo, Delivery, WindowSession, WindowEvent, WindowEndReason, WindowOutcome, AgentBackend, DirectToolSpec, ModuleSpec, FunctionSpec, ParameterSpec, ToolCall, ToolResult, } from "./agent.ts";
14
+ export type { BlobInput, BlobContent, BlobInfo, BlobStore, Segmenter, SemanticFilters, SemanticFields, Embedder, SemanticIndex, Journal, } from "./journal.ts";
15
+ export type { ModelInfo, LoginState, LoginRequest, LoginResult, RenderedPrompt, OpenWindowRequest, WindowLimits, ContinuationInfo, Delivery, WindowSession, StreamEvent, WindowEvent, WindowEndReason, WindowOutcome, AgentBackend, DirectToolSpec, ModuleSpec, FunctionSpec, ParameterSpec, JSONSchema, ToolCall, ToolResult, } from "./agent.ts";
16
16
  export type { WindowState, WindowRuntimeState, TokenUsage, Handoff, BoardLine, Expectation, InputBlock, } from "./windows.ts";
17
17
  export type { NoteStatus, Anchor, NoteWriteInput, Note, NoteSummary, NoteBody, NoteRevision, NoteContext, NoteQuery, NoteConflictInfo, ActiveSetBudget, ActiveCandidate, NoteUseKind, NoteUseInput, SnapshotOptions, NotesSnapshot, NoteDelta, NoteDeltaItem, NoteStore, } from "./notes.ts";
18
- export type { ExecResult, StagedFile, WorkspaceMount, WorkspaceQuota, Workspace, } from "./workspace.ts";
18
+ export type { ExecResult, StagedFile, OpenWorkItem, WorkspaceCheckout, WorkspaceKeep, WorkspaceMount, WorkspaceQuota, Workspace, } from "./workspace.ts";
19
19
  export type { JudgeMode, JudgeDecisionKind, CandidateWindow, JudgeTask, FilterState, RouteState, ModelTierState, NoticeState, BroadcastState, ActiveSetState, FilterRequest, RouteRequest, ModelTierRequest, NoticeRequest, BroadcastRequest, ActiveSetRequest, JudgeRequest, FilterAnswer, RouteScore, RouteAnswer, ModelTierAnswer, WindowDecision, NoticeAnswer, BroadcastAnswer, ActiveSetAnswer, JudgeAnswer, Judge, JudgeCriteria, } from "./judge.ts";
20
20
  export type { OutsideEvent, OutsideMessage, ChannelConnection, SendRequest, SendResult, Unsubscribe, Channel, } from "./channel.ts";
21
21
  export type { TaskKind, TaskInput, Task, TaskSource } from "./task.ts";
22
22
  export type { EffectKind, Effect, EffectResult, EffectExecutor } from "./effects.ts";
23
- export type { CallContext, ToolGroup } from "./tools.ts";
23
+ export type { CallContext, ToolGroup, ExternalTool, ExternalToolSource, ExternalToolChange, } from "./tools.ts";
24
24
  export type { EnvItemKind, EnvItem, EnvDraft, EnvValidation, EnvRevision, EnvDelta, Environment, } from "./environment.ts";
25
25
  export type { ServiceState, ServiceSpec, ServiceHandle, ServiceStatus, LogOptions, LogPage, Supervisor, } from "./supervisor.ts";
26
- export type { QueuedInput, InsertionRequest, InsertionPlan, RateLimitState, AdmissionRequest, AdmissionPlan, SkipPeriod, ScheduledTaskInput, ScheduledTask, ScheduledTaskUpdate, Scheduler, } from "./scheduler.ts";
27
- export type { HookOutcome, AppendContext, DispatchContext, JudgeContext, WindowStartContext, WindowEndContext, WindowContinueContext, ModelRequestContext, DeliveryContext, ToolCallContext, EffectOutcome, EffectHold, EffectContext, NoteWriteContext, ReloadContext, ServiceStateContext, Hooks, HeldEffect, } from "./hooks.ts";
26
+ export type { QueuedInput, InsertionRequest, InsertionPlan, RateLimitState, AdmissionRequest, AdmissionPlan, SkipPeriod, DailyRepeat, EveryRepeat, RepeatBounds, RepeatRule, WeeklyRepeat, ScheduledTaskInput, ScheduledTask, ScheduledTaskUpdate, Scheduler, } from "./scheduler.ts";
27
+ export type { AfterHookOutcome, HookOutcome, AppendContext, DispatchContext, JudgeContext, WindowStartContext, WindowEndContext, WindowContinueContext, ModelRequestContext, DeliveryContext, WindowStreamContext, ToolCallContext, EffectOutcome, EffectHold, EffectContext, NoteWriteContext, ReloadContext, ServiceStateContext, Hooks, HeldEffect, } from "./hooks.ts";
28
28
  export type { PluginAdapters, PromptSection, Plugin, } from "./plugin.ts";
29
29
  export type { Settings } from "./settings.ts";
package/journal.d.ts CHANGED
@@ -52,6 +52,60 @@ export interface BlobStore {
52
52
  export interface Segmenter {
53
53
  segment(text: string): string[];
54
54
  }
55
+ /** The filters a semantic search applies, as the journal's own read applies them. */
56
+ export interface SemanticFilters {
57
+ kinds?: readonly string[];
58
+ window?: WindowRef | null;
59
+ afterSeq?: number;
60
+ beforeSeq?: number;
61
+ }
62
+ /** The scalar fields a semantic filter reads, beside an event's text. */
63
+ export interface SemanticFields {
64
+ kind?: string;
65
+ window?: WindowRef | null;
66
+ }
67
+ /**
68
+ * Embedding text into vectors. The default is a small local model that needs no
69
+ * key (docs/journal.md#search); a deployment replaces it with its own, which is
70
+ * the only way text reaches anything outside the host.
71
+ */
72
+ export interface Embedder {
73
+ /** The dimension every vector this embedder returns has. */
74
+ readonly dimension: number;
75
+ /** Embed each text, in the order given. */
76
+ embed(texts: readonly string[]): Promise<readonly number[][]>;
77
+ }
78
+ /**
79
+ * The semantic channel: text search by meaning, beside the lexical indexes
80
+ * (docs/journal.md#search, docs/memory.md#search).
81
+ *
82
+ * A store takes one as an optional peer, the way it takes a `Segmenter`.
83
+ * When none is registered - the package is not installed, its vector store will
84
+ * not open on the platform, or no embedding model is available - search is
85
+ * lexical and nothing else changes. A channel that is registered but not ready
86
+ * yet - the model is still downloading, or the download failed and is waiting to
87
+ * be retried - answers `ready()` with `false`, an empty `search`, and keeps
88
+ * what `add` handed it until it is.
89
+ *
90
+ * The ref is the store's own: an event ref for the journal, a note id for the
91
+ * note store, which holds one vector per note and replaces it when the note is
92
+ * revised. What the channel keeps beside a ref - the fields, and the order the
93
+ * ref's number carries - is whatever that store's reads filter on.
94
+ */
95
+ export interface SemanticIndex {
96
+ /** Embed and store one item's text, replacing what is stored for it. */
97
+ add(ref: Ref, text: string, fields?: SemanticFields): Promise<void>;
98
+ /** Forget one item's text, as a redaction or a delete does. */
99
+ remove(ref: Ref): Promise<void>;
100
+ /** The refs nearest a query, best first, already filtered. Empty until ready. */
101
+ search(query: string, limit: number, filters: SemanticFilters): Promise<readonly Ref[]>;
102
+ /** Whether the channel can answer a query now. */
103
+ ready(): Promise<boolean>;
104
+ /** Every ref the channel holds, so a caller can index what it is missing. */
105
+ held(): Promise<ReadonlySet<Ref>>;
106
+ /** Release the underlying store. */
107
+ close(): Promise<void>;
108
+ }
55
109
  export interface Journal {
56
110
  /** Append one event. Assigns `ref` and `seq`; a redelivered `external` id is stored once. */
57
111
  append(input: EventInput): Promise<Event>;
@@ -63,7 +117,11 @@ export interface Journal {
63
117
  list(options?: ListOptions): Promise<EventPage>;
64
118
  /** The events around one, in the global order. */
65
119
  around(ref: EventRef, options?: AroundOptions): Promise<Event[]>;
66
- /** Lexical search: exact substring hits first, then word hits, each with its ref and window. */
120
+ /**
121
+ * Text search: exact substring hits first, then word hits and semantic hits
122
+ * fused by reciprocal rank, each with its ref and window
123
+ * (docs/journal.md#search).
124
+ */
67
125
  search(query: string, options?: SearchOptions): Promise<SearchPage>;
68
126
  /** Related items by weight, with the call that pages the rest. */
69
127
  links(ref: Ref, options?: LinksOptions): Promise<LinkPage>;
package/notes.d.ts CHANGED
@@ -155,6 +155,8 @@ export interface NotesSnapshot {
155
155
  limit: number;
156
156
  nextCall: string;
157
157
  budget: ActiveSetBudget;
158
+ /** What the active notes cost against `budget.tokens`, estimated. */
159
+ tokens: number;
158
160
  }
159
161
  /** What changed since an epoch: the notes written after it. */
160
162
  export interface NoteDelta {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4fu/arbor-interfaces",
3
- "version": "2026.1106.0",
3
+ "version": "2026.1107.0",
4
4
  "type": "module",
5
5
  "description": "Every replaceable interface in arbor, and the core's own shapes.",
6
6
  "exports": {
@@ -66,6 +66,6 @@
66
66
  "access": "public"
67
67
  },
68
68
  "dependencies": {
69
- "@4fu/arbor-errors": "2026.1106.0"
69
+ "@4fu/arbor-errors": "2026.1107.0"
70
70
  }
71
71
  }
package/plugin.d.ts CHANGED
@@ -5,10 +5,10 @@
5
5
  * below; every part is optional, so a plugin can be only a hook, only a skill,
6
6
  * or a whole domain such as a chat channel or a code host.
7
7
  *
8
- * Capabilities are MCP servers, not tools: a plugin adds no direct tool or
9
- * module of its own. What the model can do with it is an MCP server and skills
10
- * in `/env`, which the model reads, changes and reinstalls with `reload` like
11
- * any item.
8
+ * Capabilities the model reads, changes and reinstalls itself are MCP servers
9
+ * and skills in `/env`. A tool whose behaviour is the plugin's own code rather
10
+ * than an item in `/env` is declared as an `ExternalTool` and reaches the model
11
+ * under its own name (docs/tools.md#external-tools).
12
12
  */
13
13
  import type { Block, BlockKindSpec, TextTable } from "./blocks.ts";
14
14
  import type { Channel } from "./channel.ts";
@@ -17,6 +17,7 @@ import type { EnvDraft } from "./environment.ts";
17
17
  import type { JudgeCriteria } from "./judge.ts";
18
18
  import type { Hooks } from "./hooks.ts";
19
19
  import type { TaskSource } from "./task.ts";
20
+ import type { ExternalTool, ExternalToolSource } from "./tools.ts";
20
21
  /** Connections to the outside world. */
21
22
  export interface PluginAdapters {
22
23
  channels?: readonly Channel[];
@@ -37,6 +38,23 @@ export interface PromptSection {
37
38
  * blocks carry data, and the table carries the words.
38
39
  */
39
40
  text?: TextTable;
41
+ /**
42
+ * The section's revision, which the plugin raises when its content changes.
43
+ * The host asks for it as a window runs - one cheap call per step - and
44
+ * renders the section again only when it moved, so a section that changes
45
+ * reaches a live window as a delta carrying the changed lines
46
+ * (docs/prompting.md#plugin-sections-that-change). Absent for a section that
47
+ * never changes once the host has rendered it.
48
+ */
49
+ revision?(): string | number | Promise<string | number>;
50
+ /**
51
+ * True when the section is material from outside the framework - a third
52
+ * party's prompt text, such as the sections a DeepSeek Harness plugin
53
+ * contributes. It is marked as data, carries a lead line saying what it is,
54
+ * and the `<agent>` rules state that the framework's own prompt wins on
55
+ * conflict (docs/prompting.md#data-and-instructions).
56
+ */
57
+ outside?: boolean;
40
58
  }
41
59
  export interface Plugin {
42
60
  readonly name: string;
@@ -45,6 +63,18 @@ export interface Plugin {
45
63
  mcpServers?: readonly EnvDraft[];
46
64
  /** Skills, installed into `/env/skills` under the plugin's name. */
47
65
  skills?: readonly EnvDraft[];
66
+ /**
67
+ * Direct tools of its own: declared to the model under the names it gives
68
+ * them, and called in the turn through the same path a core tool takes
69
+ * (docs/tools.md#external-tools). A name a core tool or a module already
70
+ * declares is not registered; the host records the collision with its rule.
71
+ *
72
+ * The set may be a list or a source the host calls again on every refresh, so
73
+ * a plugin whose tools change while the host runs - the tools a harness
74
+ * around the host registered - answers with what stands now
75
+ * (docs/tools.md#external-tools-that-change).
76
+ */
77
+ tools?: readonly ExternalTool[] | ExternalToolSource;
48
78
  hooks?: Hooks;
49
79
  promptSection?: PromptSection;
50
80
  /** Extra criteria or decisions for its own inputs. */
package/plugin.js CHANGED
@@ -5,9 +5,9 @@
5
5
  * below; every part is optional, so a plugin can be only a hook, only a skill,
6
6
  * or a whole domain such as a chat channel or a code host.
7
7
  *
8
- * Capabilities are MCP servers, not tools: a plugin adds no direct tool or
9
- * module of its own. What the model can do with it is an MCP server and skills
10
- * in `/env`, which the model reads, changes and reinstalls with `reload` like
11
- * any item.
8
+ * Capabilities the model reads, changes and reinstalls itself are MCP servers
9
+ * and skills in `/env`. A tool whose behaviour is the plugin's own code rather
10
+ * than an item in `/env` is declared as an `ExternalTool` and reaches the model
11
+ * under its own name (docs/tools.md#external-tools).
12
12
  */
13
13
  export {};
package/refs.d.ts CHANGED
@@ -41,8 +41,11 @@ export type Target = string;
41
41
  export type EventKind = "input" | "tool_call" | "tool_result" | "send" | "effect_result" | "note_revision" | "window_start" | "window_end" | "delivery" | "environment_revision" | "service_state" | "redaction" | "schedule" | (string & {});
42
42
  /** Link kinds between refs. The host records them; the model never declares one. */
43
43
  export type LinkKind = "cite" | "evidence" | "range" | "origin" | "revision" | "handoff" | "continuation" | "notice" | "reply" | (string & {});
44
- /** How a search hit matched: an exact substring, or a word. */
45
- export type MatchKind = "substring" | "word";
44
+ /**
45
+ * How a search hit matched: an exact substring, a word, or meaning alone. A hit
46
+ * the semantic channel found and no lexical index did is `semantic`.
47
+ */
48
+ export type MatchKind = "substring" | "word" | "semantic";
46
49
  /** Page size default shared by every read that returns a page. */
47
50
  export declare const DEFAULT_PAGE_LIMIT = 20;
48
51
  /** Maximum page size any read returns. */
package/scheduler.d.ts CHANGED
@@ -73,15 +73,33 @@ export interface SkipPeriod {
73
73
  from: string;
74
74
  to: string;
75
75
  }
76
+ /** How a repeating schedule stops: how often it fires, and until when. */
77
+ export interface RepeatBounds {
78
+ /** How many more times it fires after the first one. */
79
+ times?: number;
80
+ /** The last moment it may fire at, in epoch milliseconds. */
81
+ until?: number;
82
+ }
83
+ /** A period in milliseconds. */
84
+ export interface EveryRepeat extends RepeatBounds {
85
+ everyMs: number;
86
+ }
87
+ /** A clock time a firing repeats at daily. */
88
+ export interface DailyRepeat extends RepeatBounds {
89
+ dailyAt: string;
90
+ }
91
+ /** Days of the week, 0 for Sunday, and the clock time it fires at on them. */
92
+ export interface WeeklyRepeat extends RepeatBounds {
93
+ weekdays: readonly number[];
94
+ at: string;
95
+ }
96
+ /** How a scheduled task repeats. */
97
+ export type RepeatRule = EveryRepeat | DailyRepeat | WeeklyRepeat;
76
98
  export interface ScheduledTaskInput {
77
99
  name?: string;
78
100
  /** The first firing, in epoch milliseconds. */
79
101
  firstAt: number;
80
- repeat?: {
81
- everyMs: number;
82
- } | {
83
- dailyAt: string;
84
- };
102
+ repeat?: RepeatRule;
85
103
  skipDaily?: readonly SkipPeriod[];
86
104
  targets?: readonly Target[];
87
105
  task: Omit<TaskInput, "kind">;
@@ -92,11 +110,9 @@ export interface ScheduledTask {
92
110
  firstAt: number;
93
111
  nextAt: number;
94
112
  lastAt?: number;
95
- repeat?: {
96
- everyMs: number;
97
- } | {
98
- dailyAt: string;
99
- };
113
+ /** How many times it has fired, so `repeat.times` survives a restart. */
114
+ fired?: number;
115
+ repeat?: RepeatRule;
100
116
  skipDaily?: readonly SkipPeriod[];
101
117
  targets: readonly Target[];
102
118
  paused: boolean;
package/settings.d.ts CHANGED
@@ -58,6 +58,21 @@ export interface Settings {
58
58
  workspaces?: {
59
59
  /** How long a chain's directory is kept after its last window ended, before the host reclaims it. */
60
60
  retentionMs?: number;
61
+ /**
62
+ * The shared folder every window works in, for the ovnfs workspace: it can
63
+ * hold several projects and repositories, and it is read-only by convention.
64
+ * Absent leaves the plain-directory workspace's own root in place.
65
+ */
66
+ shared?: string;
67
+ /** The ovnfs daemon's paths, under the host's data directory by default. */
68
+ ovnfs?: {
69
+ /** Where the daemon keeps its state. */
70
+ state?: string;
71
+ /** Where checkouts appear. */
72
+ mount?: string;
73
+ /** The ovnfs binary to run; `ovnfs` on PATH by default. */
74
+ bin?: string;
75
+ };
61
76
  };
62
77
  /** The names of the secrets a configuration may use. Never their values. */
63
78
  secrets?: readonly string[];
package/task.d.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  * or a service event. Batching is the source's business - a chat source
7
7
  * debounces per conversation, so several messages sent in a row become one
8
8
  * task. The host makes no tasks of its own for maintenance.
9
+ *
10
+ * A source claims the outside events of the source it names: the host hands it
11
+ * every event whose `OutsideEvent.source` is this source's `id`, and dispatches
12
+ * the tasks it makes of them as they stand. The host groups nothing of its own
13
+ * for a source's events, and never debounces on its behalf - a source that
14
+ * debounces keeps its own buffer.
9
15
  */
10
16
  import type { Block } from "./blocks.ts";
11
17
  import type { OutsideEvent, Unsubscribe } from "./channel.ts";
@@ -41,6 +47,9 @@ export interface TaskSource {
41
47
  readonly id: string;
42
48
  /** Turn outside events into tasks, batched the way this source batches. */
43
49
  toTasks(events: readonly OutsideEvent[]): readonly TaskInput[];
44
- /** Push source. The host may poll instead. */
50
+ /**
51
+ * Push source. The host may poll instead. The host takes this one at `start`,
52
+ * the way it takes a channel's, and what it pushes goes through `ingest`.
53
+ */
45
54
  subscribe?(handler: (events: readonly OutsideEvent[]) => void | Promise<void>): Unsubscribe;
46
55
  }
package/task.js CHANGED
@@ -6,5 +6,11 @@
6
6
  * or a service event. Batching is the source's business - a chat source
7
7
  * debounces per conversation, so several messages sent in a row become one
8
8
  * task. The host makes no tasks of its own for maintenance.
9
+ *
10
+ * A source claims the outside events of the source it names: the host hands it
11
+ * every event whose `OutsideEvent.source` is this source's `id`, and dispatches
12
+ * the tasks it makes of them as they stand. The host groups nothing of its own
13
+ * for a source's events, and never debounces on its behalf - a source that
14
+ * debounces keeps its own buffer.
9
15
  */
10
16
  export {};
package/tools.d.ts CHANGED
@@ -4,10 +4,12 @@
4
4
  *
5
5
  * Decisions and plain commands are direct tools, declared to the model and
6
6
  * called in the turn. Everything else is a module function, called from a
7
- * script in `code_exec`. Plugins add no tools of their own; what the model can
8
- * do with a plugin is an MCP server and skills in `/env`.
7
+ * script in `code_exec`. A plugin declares its own direct tools as
8
+ * `ExternalTool`s, which the host registers as a group of their own; what the
9
+ * model can otherwise do with a plugin is an MCP server and skills in `/env`.
9
10
  */
10
11
  import type { DirectToolSpec, FunctionSpec, ModuleSpec, ParameterSpec, ToolCall, ToolResult } from "./agent.ts";
12
+ import type { JSONSchema } from "./agent.ts";
11
13
  import type { EventRef, WindowRef } from "./refs.ts";
12
14
  import type { WorkspaceMount } from "./workspace.ts";
13
15
  /** What a call is made in. The service layer fills it; a group never reaches past it. */
@@ -31,3 +33,50 @@ export interface ToolGroup {
31
33
  call(request: ToolCall, context: CallContext): Promise<ToolResult>;
32
34
  }
33
35
  export type { DirectToolSpec, FunctionSpec, ModuleSpec, ParameterSpec, ToolCall, ToolResult };
36
+ /**
37
+ * One direct tool declared from outside the core: a plugin's own, or a
38
+ * backend's injection of the tools an outside harness registered
39
+ * (docs/tools.md#external-tools).
40
+ *
41
+ * One shape for both, so a plugin and a backend share one path instead of two:
42
+ * the host registers what it is handed, and from there the tool is a direct
43
+ * tool like any other - declared to the model under its own name, and called
44
+ * through the same hooks, journaling, scrub and result cut as the core's own.
45
+ * Nothing about it is special-cased after registration.
46
+ */
47
+ export interface ExternalTool {
48
+ readonly name: string;
49
+ /** What it does and when to use it; side effects. 1 to 4 sentences. */
50
+ readonly description: string;
51
+ /**
52
+ * The parameters in arbor's own declaration shape. Optional for a tool that
53
+ * declares `schema` instead: an outside harness already has a JSON Schema for
54
+ * its tool, and restating it in this shape would only lose what the schema
55
+ * says.
56
+ */
57
+ readonly parameters?: Record<string, ParameterSpec>;
58
+ /**
59
+ * The parameters as the JSON Schema the outside harness declared them,
60
+ * carried verbatim (docs/tools.md#external-tools). What the model reads when
61
+ * the tool has one; `parameters` is the declaration for a tool that has none.
62
+ */
63
+ readonly schema?: JSONSchema;
64
+ /** Run one call. The same path a core tool takes. */
65
+ execute(request: ToolCall, context: CallContext): Promise<ToolResult>;
66
+ }
67
+ /**
68
+ * Where a plugin's external tools come from: a list, or a call that answers
69
+ * with the current one. The host reads the source again on every refresh, so a
70
+ * plugin whose tools change at runtime - the tools a harness around the host
71
+ * registered - answers with what stands now
72
+ * (docs/tools.md#external-tools-that-change).
73
+ */
74
+ export type ExternalToolSource = () => readonly ExternalTool[] | Promise<readonly ExternalTool[]>;
75
+ /** What one refresh of the external tools changed about one tool. */
76
+ export interface ExternalToolChange {
77
+ change: "added" | "removed";
78
+ /** The tool's own name, as the model calls it. */
79
+ name: string;
80
+ /** Who declares it: the plugin's name, or the backend's. */
81
+ source: string;
82
+ }
package/tools.js CHANGED
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Decisions and plain commands are direct tools, declared to the model and
6
6
  * called in the turn. Everything else is a module function, called from a
7
- * script in `code_exec`. Plugins add no tools of their own; what the model can
8
- * do with a plugin is an MCP server and skills in `/env`.
7
+ * script in `code_exec`. A plugin declares its own direct tools as
8
+ * `ExternalTool`s, which the host registers as a group of their own; what the
9
+ * model can otherwise do with a plugin is an MCP server and skills in `/env`.
9
10
  */
10
11
  export {};
package/windows.d.ts CHANGED
@@ -57,6 +57,11 @@ export interface BoardLine {
57
57
  expects: readonly Expectation[];
58
58
  state: WindowState;
59
59
  usage: TokenUsage;
60
+ /**
61
+ * The work this window still has open in its workspace, each item as the
62
+ * model reads it (docs/workspaces.md#open-work). Empty when it has none.
63
+ */
64
+ openWork?: readonly string[];
60
65
  }
61
66
  /**
62
67
  * What a window expects to arrive, so the input reaches it instead of a new
package/workspace.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { TextTable } from "./blocks.ts";
10
10
  import type { WindowRef, WorkspaceRef } from "./refs.ts";
11
+ import type { PromptSection } from "./plugin.ts";
11
12
  import type { ToolGroup } from "./tools.ts";
12
13
  /** The result of one command run in a mounted workspace. */
13
14
  export interface ExecResult {
@@ -79,6 +80,46 @@ export interface WorkspaceQuota {
79
80
  used: number;
80
81
  files: number;
81
82
  }
83
+ /**
84
+ * One checkout a window's chain has open, for an implementation that gives a
85
+ * window a copy-on-write checkout rather than working in place (the ovnfs
86
+ * workspace, docs/workspaces.md). `dirty` is true when the checkout holds
87
+ * changes not yet applied back, which counts as open work at `end`.
88
+ */
89
+ export interface WorkspaceCheckout {
90
+ /** The checkout's name, which begins with the window ref that opened it. */
91
+ name: string;
92
+ /** The checkout's path on the host, where a command works in it. */
93
+ path: string;
94
+ /** The checkout holds changes not yet applied back to the shared folder. */
95
+ dirty: boolean;
96
+ }
97
+ /**
98
+ * One persistent directory an implementation keeps beyond a window's own
99
+ * folder: a named place whose content outlives the window that made it, the
100
+ * retention period and a host restart, and which a later window finds by name
101
+ * and uses. The plain directory workspace keeps them under one root beside the
102
+ * windows' own directories (docs/workspaces.md#persistent-directories).
103
+ */
104
+ export interface WorkspaceKeep {
105
+ /** The name the model gave it, which is how a later window names it. */
106
+ name: string;
107
+ /** The directory's path on the host, where a command works in it. */
108
+ path: string;
109
+ }
110
+ /**
111
+ * One piece of work a window still has open in its workspace: something it
112
+ * started and has not finished. The ovnfs workspace answers with its unapplied
113
+ * checkouts; an implementation whose work is finished in place - the plain
114
+ * directory workspace, which a continuation keeps working in - answers nothing
115
+ * (docs/workspaces.md#open-work).
116
+ */
117
+ export interface OpenWorkItem {
118
+ /** The name the implementation knows it by, which is how a later window names it. */
119
+ name: string;
120
+ /** What is open about it, in the implementation's own words, when it has any. */
121
+ detail?: string;
122
+ }
82
123
  export interface Workspace {
83
124
  /**
84
125
  * Create the window's own directory, on first use. A window that never
@@ -110,4 +151,54 @@ export interface Workspace {
110
151
  * about it (docs/workspaces.md, docs/architecture.md#plugins).
111
152
  */
112
153
  groups?: (table: TextTable) => readonly ToolGroup[];
154
+ /**
155
+ * The open checkouts of a window's chain, for `end`'s open-work check, for
156
+ * `workspace.list` and `workspace.path`, and as the gate reclaim uses. Absent
157
+ * for an implementation that keeps no checkouts of its own.
158
+ */
159
+ checkouts?(window: WindowRef): Promise<readonly WorkspaceCheckout[]>;
160
+ /**
161
+ * The work a window still has open: what it started and has not finished.
162
+ * The host asks for it where a window's own state is not enough - the
163
+ * reminder tiers name the open items, the board shows them, `end` refuses
164
+ * without a handoff while any are open, and a continuation's first input
165
+ * names the ones its predecessor left (docs/windows.md#usage-reminders,
166
+ * docs/workspaces.md#open-work). Absent for an implementation that keeps
167
+ * nothing open.
168
+ */
169
+ openWork?(window: WindowRef): Promise<readonly OpenWorkItem[]>;
170
+ /**
171
+ * The persistent directories a window's chain created, or every one of them
172
+ * when no window is named - the places whose content outlives the window, the
173
+ * retention period and a host restart, which a later window finds by name and
174
+ * uses (docs/workspaces.md#persistent-directories). Absent for an
175
+ * implementation that keeps none. The host asks for a chain's to name them in
176
+ * a continuation's first input; a window asks for all of them through the
177
+ * implementation's own module function.
178
+ */
179
+ keeps?(window?: WindowRef): Promise<readonly WorkspaceKeep[]>;
180
+ /**
181
+ * The prompt section this implementation contributes, or none. It carries the
182
+ * method the model follows - how this implementation wants files changed -
183
+ * rendered in its own tag, so each implementation owns its own prompt text and
184
+ * a host that fell back to another implementation shows that one's instead.
185
+ */
186
+ promptSection?(table: TextTable): PromptSection;
187
+ /**
188
+ * The environment a bash command in this window runs with, beyond the host's
189
+ * own, or none. The ovnfs workspace sets `ARBOR_WINDOW` and the git hook path
190
+ * that stamps `Arbor-Window` onto commits made in a checkout
191
+ * (docs/workspaces.md). Set on the command's own process, never on the host,
192
+ * so it does not reach an MCP server or a service.
193
+ */
194
+ bashEnv?(window: WindowRef): Record<string, string>;
195
+ /**
196
+ * The built-in skills this implementation seeds into the environment, read
197
+ * only, or none. The ovnfs workspace seeds the `ovnfs` skill that documents
198
+ * the full CLI method.
199
+ */
200
+ builtinSkills?(): readonly {
201
+ name: string;
202
+ source: string;
203
+ }[];
113
204
  }