@4fu/arbor-interfaces 0.1.0 → 2026.1106.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.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.
@@ -118,7 +120,8 @@ window gets, and how a continuation keeps working in it, are in
118
120
  | [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
119
121
  | [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
120
122
  | [`@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 |
123
+ | [`@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 |
124
+ | [`@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
125
  | [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
123
126
  | [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
124
127
  | [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
@@ -151,7 +154,7 @@ deployment can see what it would be taking on before it adopts one.
151
154
  | [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
152
155
  | [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
153
156
  | [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 |
157
+ | [docs/workspaces.md](docs/workspaces.md) | the shared folder and per-window checkouts: isolation, continuity, what the agent does |
155
158
  | [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
156
159
  | [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
157
160
  | [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
package/agent.d.ts CHANGED
@@ -1,14 +1,15 @@
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";
@@ -80,6 +81,12 @@ export interface ContinuationInfo {
80
81
  /** What the host inserts into a running window at a tool-call boundary. */
81
82
  export interface Delivery {
82
83
  kind: "inserted_task" | "conversation_update" | "notice" | "broadcast" | "window_message" | "environment_delta"
84
+ /**
85
+ * A plugin's prompt section changed since this window's prefix was built:
86
+ * the lines that changed, and nothing that did not
87
+ * (docs/prompting.md#plugin-sections-that-change).
88
+ */
89
+ | "prompt_delta"
83
90
  /** A host usage reminder, appended to an input (docs/windows.md#usage-reminders). */
84
91
  | "reminder";
85
92
  /** The window it comes from, for a notice, a broadcast or a window message. */
@@ -102,13 +109,40 @@ export interface WindowSession {
102
109
  pause(): Promise<void>;
103
110
  resume(): Promise<void>;
104
111
  usage(): TokenUsage;
105
- /** Tool calls, results and appends as they happen. */
112
+ /** Tool calls, results and what the model is writing, as they happen. */
106
113
  events(): AsyncIterable<WindowEvent>;
107
114
  /** Resolves when the window ends, with why. */
108
115
  outcome(): Promise<WindowOutcome>;
109
116
  }
117
+ /**
118
+ * What a window is writing while it runs, one delta at a time
119
+ * (docs/windows.md#streaming).
120
+ *
121
+ * Streams are best-effort and ephemeral: they are not journaled, and the
122
+ * journal's durable events stay the only record. A backend that cannot report
123
+ * deltas never emits them, and a consumer that misses one keeps going - the
124
+ * next delta carries what stands so far.
125
+ */
126
+ export type StreamEvent =
127
+ /** Assistant text, as the model writes it. */
128
+ {
129
+ kind: "text_delta";
130
+ delta: string;
131
+ }
132
+ /**
133
+ * One tool call's arguments, as the model writes them. `call.args` is what
134
+ * has been decoded so far and is best-effort: a call still being written is
135
+ * not valid JSON yet, so a string argument holds the characters written so
136
+ * far - which is what makes `send`'s content visible while the model is
137
+ * still writing it (docs/tools.md#direct-tools). The durable `tool_call`
138
+ * event carries the complete arguments, once.
139
+ */
140
+ | {
141
+ kind: "tool_call_delta";
142
+ call: ToolCall;
143
+ };
110
144
  /** What a backend streams while a window runs. */
111
- export type WindowEvent = {
145
+ export type WindowEvent = StreamEvent | {
112
146
  kind: "tool_call";
113
147
  call: ToolCall;
114
148
  } | {
@@ -153,6 +187,14 @@ export interface AgentBackend {
153
187
  * reports nothing is never limited.
154
188
  */
155
189
  rateLimits?(): Promise<RateLimitState> | RateLimitState;
190
+ /**
191
+ * Direct tools the backend injects from outside arbor: the tools an outside
192
+ * harness registered, which a host embedded in that harness hands in here
193
+ * (docs/tools.md#external-tools). They are declared to the model and called
194
+ * in the turn like any other direct tool. Read once, before the first window
195
+ * opens.
196
+ */
197
+ externalTools?(): readonly ExternalTool[] | Promise<readonly ExternalTool[]>;
156
198
  /** Open one window and start its first turn. */
157
199
  open(request: OpenWindowRequest): Promise<WindowSession>;
158
200
  }
@@ -162,6 +204,12 @@ export interface DirectToolSpec {
162
204
  /** What it does and when to use it; side effects. 1 to 4 sentences. */
163
205
  description: string;
164
206
  parameters: Record<string, ParameterSpec>;
207
+ /**
208
+ * Who declares it, when it is not the core's own: a plugin's name, or a
209
+ * backend's. The tools list says where such a tool comes from, so the model
210
+ * knows whose it is (docs/tools.md#external-tools).
211
+ */
212
+ source?: string;
165
213
  }
166
214
  /** Called from a script in `code_exec` (docs/tools.md#modules). */
167
215
  export interface ModuleSpec {
@@ -193,6 +241,12 @@ export interface ToolResult {
193
241
  ok: boolean;
194
242
  /** Stable fields, bounded, in the core's error shape when it failed. */
195
243
  content: string;
244
+ /**
245
+ * The tool that was called, as the prompt names it. A refusal carries it so
246
+ * the error line starts with the tool name; the catalog fills it on the way
247
+ * out, so a group that leaves it out still names the call.
248
+ */
249
+ tool?: string;
196
250
  error?: {
197
251
  rule: string;
198
252
  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
@@ -151,7 +151,7 @@ export interface TraceNode {
151
151
  /** A target a window is behind on. */
152
152
  export interface TargetGap {
153
153
  target: Target;
154
- /** The newest event on the target. */
154
+ /** The newest event on the target another window wrote after this one opened. */
155
155
  latest: number;
156
156
  /** The newest event on the target the window has seen. */
157
157
  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
@@ -12,18 +12,18 @@ export type { BlockValue, Block, BlockKind, BlockFieldSpec, BlockKindSpec, TextT
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
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";
15
+ export type { ModelInfo, LoginState, LoginRequest, LoginResult, RenderedPrompt, OpenWindowRequest, WindowLimits, ContinuationInfo, Delivery, WindowSession, StreamEvent, WindowEvent, WindowEndReason, WindowOutcome, AgentBackend, DirectToolSpec, ModuleSpec, FunctionSpec, ParameterSpec, 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 } 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/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": "0.1.0",
3
+ "version": "2026.1106.1",
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": "0.1.0"
69
+ "@4fu/arbor-errors": "2026.1106.1"
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 } 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,13 @@ 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
+ tools?: readonly ExternalTool[];
48
73
  hooks?: Hooks;
49
74
  promptSection?: PromptSection;
50
75
  /** 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/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,8 +4,9 @@
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";
11
12
  import type { EventRef, WindowRef } from "./refs.ts";
@@ -31,3 +32,22 @@ export interface ToolGroup {
31
32
  call(request: ToolCall, context: CallContext): Promise<ToolResult>;
32
33
  }
33
34
  export type { DirectToolSpec, FunctionSpec, ModuleSpec, ParameterSpec, ToolCall, ToolResult };
35
+ /**
36
+ * One direct tool declared from outside the core: a plugin's own, or a
37
+ * backend's injection of the tools an outside harness registered
38
+ * (docs/tools.md#external-tools).
39
+ *
40
+ * One shape for both, so a plugin and a backend share one path instead of two:
41
+ * the host registers what it is handed, and from there the tool is a direct
42
+ * tool like any other - declared to the model under its own name, and called
43
+ * through the same hooks, journaling, scrub and result cut as the core's own.
44
+ * Nothing about it is special-cased after registration.
45
+ */
46
+ export interface ExternalTool {
47
+ readonly name: string;
48
+ /** What it does and when to use it; side effects. 1 to 4 sentences. */
49
+ readonly description: string;
50
+ readonly parameters: Record<string, ParameterSpec>;
51
+ /** Run one call. The same path a core tool takes. */
52
+ execute(request: ToolCall, context: CallContext): Promise<ToolResult>;
53
+ }
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
  }