@4fu/arbor-interfaces 0.0.0-stage → 2026.1106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +187 -2
- package/agent.d.ts +202 -0
- package/agent.js +11 -0
- package/blocks.d.ts +56 -0
- package/blocks.js +11 -0
- package/channel.d.ts +61 -0
- package/channel.js +11 -0
- package/effects.d.ts +42 -0
- package/effects.js +11 -0
- package/environment.d.ts +109 -0
- package/environment.js +16 -0
- package/events.d.ts +162 -0
- package/events.js +4 -0
- package/hooks.d.ts +141 -0
- package/hooks.js +10 -0
- package/index.d.ts +29 -0
- package/index.js +10 -0
- package/journal.d.ts +87 -0
- package/journal.js +16 -0
- package/judge.d.ts +206 -0
- package/judge.js +13 -0
- package/notes.d.ts +215 -0
- package/notes.js +5 -0
- package/package.json +69 -4
- package/plugin.d.ts +52 -0
- package/plugin.js +13 -0
- package/refs.d.ts +49 -0
- package/refs.js +9 -0
- package/scheduler.d.ts +132 -0
- package/scheduler.js +21 -0
- package/settings.d.ts +64 -0
- package/settings.js +9 -0
- package/supervisor.d.ts +84 -0
- package/supervisor.js +11 -0
- package/task.d.ts +46 -0
- package/task.js +10 -0
- package/tools.d.ts +33 -0
- package/tools.js +10 -0
- package/windows.d.ts +81 -0
- package/windows.js +8 -0
- package/workspace.d.ts +113 -0
- package/workspace.js +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 4fuu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,188 @@
|
|
|
1
|
-
#
|
|
1
|
+
# arbor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
arbor is a framework for an agent without sessions: many context windows run
|
|
4
|
+
at once over one shared record, so work moves in parallel, replies come fast,
|
|
5
|
+
and every window can find everything that was ever done.
|
|
6
|
+
|
|
7
|
+
A deployment composes the pieces it wants and registers plugins for the domains
|
|
8
|
+
it serves. The framework guarantees what an agent cannot guarantee for itself -
|
|
9
|
+
the record, isolation, exactly-once effects, the checks, dispatch - and leaves
|
|
10
|
+
everything that takes judgment, such as git, merges and conflicts, to the agent.
|
|
11
|
+
|
|
12
|
+
## Core ideas
|
|
13
|
+
|
|
14
|
+
- **No sessions to manage.** Work runs in disposable context windows. Any
|
|
15
|
+
number run at the same time; none of them owns anything, and a window that
|
|
16
|
+
ends loses nothing. The model looks up what it needs instead of carrying it.
|
|
17
|
+
- **One record, shared live.** Every input, tool call, result, send and note
|
|
18
|
+
revision goes into one journal and is visible to every window as
|
|
19
|
+
soon as it is written. Records never merge and are never lost.
|
|
20
|
+
- **Dispatch by a judge.** A judge (Jev by default, replaceable) decides which
|
|
21
|
+
live window takes a new input, or opens a new one. There is no router session
|
|
22
|
+
and no main line.
|
|
23
|
+
- **Current without locks.** Windows run in parallel. New events on a
|
|
24
|
+
conversation a window is in are steered into it as they arrive; only a note
|
|
25
|
+
write is checked against the revision the window read.
|
|
26
|
+
- **Mechanisms in the framework, policy in the agent.** The framework
|
|
27
|
+
guarantees what an agent cannot guarantee for itself - the record, isolation,
|
|
28
|
+
exactly-once effects, the checks, dispatch. Everything that takes judgment,
|
|
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.
|
|
33
|
+
- **A self-changing environment.** The model adds and changes skills, MCP
|
|
34
|
+
servers and resident services itself; a `reload` validates the change and
|
|
35
|
+
broadcasts it to every live window as a delta of names and descriptions.
|
|
36
|
+
- **Minimal limits.** The framework sets no policy. Permissions, privacy
|
|
37
|
+
review, budgets and approvals belong to deployments built on it, through
|
|
38
|
+
hooks.
|
|
39
|
+
- **Direct tools plus code mode.** Decisions are direct tools; everything else
|
|
40
|
+
is a module called from `code_exec`. There is no CLI.
|
|
41
|
+
- **Every module is replaceable.** The agent backend (pi by default), the
|
|
42
|
+
journal, notes, workspaces, the judge, channels and the rest are interfaces
|
|
43
|
+
([docs/architecture.md](docs/architecture.md#interfaces)).
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Node 24 or newer. Every package is published separately, so a deployment
|
|
48
|
+
installs the core and the defaults it wants:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm install @4fu/arbor-core @4fu/arbor-journal-sqlite @4fu/arbor-notes-sqlite \
|
|
52
|
+
@4fu/arbor-environment-dir @4fu/arbor-supervisor-process @4fu/arbor-agent-pi \
|
|
53
|
+
@4fu/arbor-workspace-dir
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`@4fu/arbor-plugin-http` is optional; it is the example plugin below.
|
|
57
|
+
`@4fu/arbor-segmenter-jieba` is an optional peer of the journal.
|
|
58
|
+
|
|
59
|
+
## A minimal host
|
|
60
|
+
|
|
61
|
+
The smallest host needs a journal and a backend. Adding the defaults and one
|
|
62
|
+
plugin is what a deployment usually starts from:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { mkdtemp } from "node:fs/promises";
|
|
66
|
+
import { tmpdir } from "node:os";
|
|
67
|
+
import { join } from "node:path";
|
|
68
|
+
import { Arbor } from "@4fu/arbor-core";
|
|
69
|
+
import { DirectoryEnvironment } from "@4fu/arbor-environment-dir";
|
|
70
|
+
import { SqliteJournal } from "@4fu/arbor-journal-sqlite";
|
|
71
|
+
import { SqliteNoteStore } from "@4fu/arbor-notes-sqlite";
|
|
72
|
+
import { ProcessSupervisor } from "@4fu/arbor-supervisor-process";
|
|
73
|
+
import { DirectoryWorkspace } from "@4fu/arbor-workspace-dir";
|
|
74
|
+
import { PiAgentBackend } from "@4fu/arbor-agent-pi";
|
|
75
|
+
import { HttpChannel, httpPlugin } from "@4fu/arbor-plugin-http";
|
|
76
|
+
|
|
77
|
+
const root = await mkdtemp(join(tmpdir(), "arbor-"));
|
|
78
|
+
|
|
79
|
+
// Every module below is a default implementation of an interface. Replace any
|
|
80
|
+
// one by passing your own; the host does not change.
|
|
81
|
+
const journal = new SqliteJournal({ path: join(root, "journal.sqlite") });
|
|
82
|
+
const notes = new SqliteNoteStore({ path: join(root, "notes.sqlite"), journal });
|
|
83
|
+
const supervisor = new ProcessSupervisor({ root: join(root, "services") });
|
|
84
|
+
const environment = new DirectoryEnvironment({ root: join(root, "env"), supervisor });
|
|
85
|
+
const workspace = new DirectoryWorkspace({ root: join(root, "workspaces") });
|
|
86
|
+
|
|
87
|
+
// A plugin adds; it never patches the core. This one is a chat channel: a
|
|
88
|
+
// webhook in, a window's sends out, and a page that drives the whole host.
|
|
89
|
+
let arbor;
|
|
90
|
+
const channel = new HttpChannel({ host: "127.0.0.1", port: 8787, board: () => arbor.boardLines() });
|
|
91
|
+
arbor = new Arbor({
|
|
92
|
+
journal,
|
|
93
|
+
notes,
|
|
94
|
+
backend: new PiAgentBackend(),
|
|
95
|
+
workspace,
|
|
96
|
+
environment,
|
|
97
|
+
supervisor,
|
|
98
|
+
plugins: [httpPlugin({ channel })],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await arbor.start();
|
|
102
|
+
await channel.listen();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`arbor.open(task)` dispatches one task, and the journal is where everything that
|
|
106
|
+
happened can be read back ([docs/journal.md](docs/journal.md)). The workspace a
|
|
107
|
+
window gets, and how a continuation keeps working in it, are in
|
|
108
|
+
[docs/workspaces.md](docs/workspaces.md).
|
|
109
|
+
|
|
110
|
+
## Packages
|
|
111
|
+
|
|
112
|
+
| Package | What it is |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| [`@4fu/arbor-interfaces`](packages/interfaces) | every replaceable interface, plus the core's own shapes. Types only. |
|
|
115
|
+
| [`@4fu/arbor-errors`](packages/errors) | the error shape every refusal uses: the rule and the next action |
|
|
116
|
+
| [`@4fu/arbor-prompt`](packages/prompt) | typed blocks, the one deterministic renderer and the replaceable text table |
|
|
117
|
+
| [`@4fu/arbor-tools`](packages/tools) | the entry points: direct tools, code mode modules, the host API |
|
|
118
|
+
| [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
|
|
119
|
+
| [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
|
|
120
|
+
| [`@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 |
|
|
122
|
+
| [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
|
|
123
|
+
| [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
|
|
124
|
+
| [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
125
|
+
| [`@4fu/arbor-agent-pi`](packages/agent-pi) | the default `AgentBackend`: pi-agent-core in process |
|
|
126
|
+
| [`@4fu/arbor-plugin-http`](packages/plugin-http) | the HTTP test channel: a webhook in, sends out, a page that drives the whole host |
|
|
127
|
+
| [`@4fu/arbor-core`](packages/core) | the service layer: one implementation of every operation - the window lifecycle, dispatch, coordination, the board, notes access, workspaces, effects and schedules |
|
|
128
|
+
|
|
129
|
+
### Defaults are examples
|
|
130
|
+
|
|
131
|
+
Every default above is a reference implementation of its interface, written to
|
|
132
|
+
be read and replaced. A real deployment is expected to swap out the ones that do
|
|
133
|
+
not fit: the SQLite journal and notes, the directory workspace, the process
|
|
134
|
+
supervisor, the environment directory, the HTTP test plugin and the Jev judge
|
|
135
|
+
are all examples of what an interface can be satisfied by, not commitments.
|
|
136
|
+
What a deployment keeps is its own choice, and the interfaces are the contract
|
|
137
|
+
([docs/architecture.md](docs/architecture.md#interfaces)).
|
|
138
|
+
|
|
139
|
+
Each package's own documentation holds the limits of that default, so a
|
|
140
|
+
deployment can see what it would be taking on before it adopts one.
|
|
141
|
+
|
|
142
|
+
## Documents
|
|
143
|
+
|
|
144
|
+
| Page | Contents |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| [docs/overview.md](docs/overview.md) | goals, concepts, principles, routes considered |
|
|
147
|
+
| [docs/architecture.md](docs/architecture.md) | layers, interfaces, stack, models, plugins, hooks, data placement |
|
|
148
|
+
| [docs/journal.md](docs/journal.md) | events and their order, targets, watermarks, external ids, refs, tracing, media, redaction |
|
|
149
|
+
| [docs/windows.md](docs/windows.md) | window lifecycle, prompt layout, reminders, continuation |
|
|
150
|
+
| [docs/dispatch.md](docs/dispatch.md) | task sources, judge decisions, insertion timing, backpressure, scheduled tasks |
|
|
151
|
+
| [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
|
|
152
|
+
| [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
|
|
153
|
+
| [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 |
|
|
155
|
+
| [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
|
|
156
|
+
| [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
|
|
157
|
+
| [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
|
|
158
|
+
|
|
159
|
+
## Repository layout
|
|
160
|
+
|
|
161
|
+
`pnpm check` typechecks and tests every package on its TypeScript sources.
|
|
162
|
+
`pnpm build` emits the `dist` publishing needs, and `pnpm pack` shows what each
|
|
163
|
+
tarball would contain ([docs/release.md](docs/release.md)).
|
|
164
|
+
|
|
165
|
+
| Path | Layer |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `packages/interfaces` | the interfaces layer: every replaceable interface, plus the core's own shapes. Types only. |
|
|
168
|
+
| `packages/errors` | the error shape every refusal uses: the rule and the next action |
|
|
169
|
+
| `packages/journal-sqlite` | the default `Journal` and `BlobStore` on SQLite |
|
|
170
|
+
| `packages/segmenter-jieba` | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
|
|
171
|
+
| `packages/notes-sqlite` | the default `NoteStore` on SQLite, beside the journal |
|
|
172
|
+
| `packages/judge-jev` | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
173
|
+
| `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 |
|
|
174
|
+
| `packages/environment-dir` | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
|
|
175
|
+
| `packages/supervisor-process` | the default `Supervisor`: one host process per service, restart with backoff, logs |
|
|
176
|
+
| `packages/plugin-http` | the HTTP test channel: a webhook in, sends out, a page that drives the whole host |
|
|
177
|
+
| `packages/prompt` | typed blocks, the one deterministic renderer and the replaceable text table |
|
|
178
|
+
| `packages/agent-pi` | the default `AgentBackend`: pi-agent-core in process |
|
|
179
|
+
| `packages/tools` | the entry points: direct tools, code mode modules, the host API |
|
|
180
|
+
| `packages/core` | the service layer: one implementation of every operation - the window lifecycle, dispatch, coordination, the board, notes access, workspaces, effects, schedules and the snapshot epochs |
|
|
181
|
+
|
|
182
|
+
`mise.toml` pins the toolchain - Node and pnpm - installed under
|
|
183
|
+
`~/.local/share/mise`, and `.agents/setup` prepares a machine from it. A machine
|
|
184
|
+
without mise is expected to carry Node 24 or newer and pnpm 12 already.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT, see [LICENSE](LICENSE).
|
package/agent.d.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
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).
|
|
6
|
+
*
|
|
7
|
+
* arbor uses pi-agent-core's `Agent` with its own system prompt: pi's default
|
|
8
|
+
* coding prompt, context files and skills never reach the model
|
|
9
|
+
* (docs/prompting.md#no-built-in-pi-prompt).
|
|
10
|
+
*/
|
|
11
|
+
import type { Block } from "./blocks.ts";
|
|
12
|
+
import type { TaskRef, Ref, WindowRef } from "./refs.ts";
|
|
13
|
+
import type { Handoff, TokenUsage, WindowState } from "./windows.ts";
|
|
14
|
+
import type { RateLimitState } from "./scheduler.ts";
|
|
15
|
+
/** One model the backend can run. Only models that take images are offered. */
|
|
16
|
+
export interface ModelInfo {
|
|
17
|
+
id: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
/** The tier settings map this model to, when the backend knows one. */
|
|
20
|
+
tier?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
images: boolean;
|
|
23
|
+
}
|
|
24
|
+
export type LoginState = "logged-in" | "logged-out" | "failed";
|
|
25
|
+
export interface LoginRequest {
|
|
26
|
+
provider: string;
|
|
27
|
+
/** The name of a secret; a configuration never holds a secret's value. */
|
|
28
|
+
secret?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface LoginResult {
|
|
31
|
+
provider: string;
|
|
32
|
+
state: LoginState;
|
|
33
|
+
models: ModelInfo[];
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The prompt a window is opened with: the frozen prefix of its epoch. */
|
|
37
|
+
export interface RenderedPrompt {
|
|
38
|
+
/** `<identity>`, `<agent>`, `<tools>`, `<modules>`, `<env>`, plugin sections. */
|
|
39
|
+
system: string;
|
|
40
|
+
/** The active notes, by summary. */
|
|
41
|
+
memory: string;
|
|
42
|
+
/** The environment revision the prompt was rendered against. */
|
|
43
|
+
envRevision: string;
|
|
44
|
+
}
|
|
45
|
+
/** What a window is opened for. */
|
|
46
|
+
export interface OpenWindowRequest {
|
|
47
|
+
window: WindowRef;
|
|
48
|
+
task: TaskRef;
|
|
49
|
+
prompt: RenderedPrompt;
|
|
50
|
+
tools: readonly DirectToolSpec[];
|
|
51
|
+
modules: readonly ModuleSpec[];
|
|
52
|
+
/** The model the tier maps to, resolved from the backend's catalog. */
|
|
53
|
+
model: string;
|
|
54
|
+
/**
|
|
55
|
+
* The first input, already rendered by the host's one renderer: the task,
|
|
56
|
+
* the board lines related to it and, for a continuation, the previous
|
|
57
|
+
* window's ref and handoff (docs/prompting.md#layers).
|
|
58
|
+
*/
|
|
59
|
+
firstInput?: string;
|
|
60
|
+
/** The workspace mounted for this window, when it has one. */
|
|
61
|
+
workspace?: string;
|
|
62
|
+
limits: WindowLimits;
|
|
63
|
+
continuation?: ContinuationInfo;
|
|
64
|
+
}
|
|
65
|
+
export interface WindowLimits {
|
|
66
|
+
/** The context window's token budget. */
|
|
67
|
+
contextTokens: number;
|
|
68
|
+
/** Tokens held back from the model, for the final reminder. */
|
|
69
|
+
reserveTokens: number;
|
|
70
|
+
/** How long an ended run stays open for a related input, in seconds. */
|
|
71
|
+
idleSeconds: number;
|
|
72
|
+
}
|
|
73
|
+
/** A continuation's first input carries these. */
|
|
74
|
+
export interface ContinuationInfo {
|
|
75
|
+
previous: WindowRef;
|
|
76
|
+
handoff?: Handoff;
|
|
77
|
+
/** Set after the hard limit: the window was reset mid-task. */
|
|
78
|
+
midTaskReset?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** What the host inserts into a running window at a tool-call boundary. */
|
|
81
|
+
export interface Delivery {
|
|
82
|
+
kind: "inserted_task" | "conversation_update" | "notice" | "broadcast" | "window_message" | "environment_delta"
|
|
83
|
+
/** A host usage reminder, appended to an input (docs/windows.md#usage-reminders). */
|
|
84
|
+
| "reminder";
|
|
85
|
+
/** The window it comes from, for a notice, a broadcast or a window message. */
|
|
86
|
+
from?: WindowRef;
|
|
87
|
+
refs: readonly Ref[];
|
|
88
|
+
time: number;
|
|
89
|
+
content: readonly Block[];
|
|
90
|
+
}
|
|
91
|
+
/** One running window, owned by the backend. */
|
|
92
|
+
export interface WindowSession {
|
|
93
|
+
readonly window: WindowRef;
|
|
94
|
+
readonly state: WindowState;
|
|
95
|
+
/** Insert at the next tool-call boundary, through the backend's steering. */
|
|
96
|
+
steer(delivery: Delivery): Promise<void>;
|
|
97
|
+
/** Queue for after the run ends. */
|
|
98
|
+
followUp(delivery: Delivery): Promise<void>;
|
|
99
|
+
/** End the run. With a handoff the host opens a continuation. */
|
|
100
|
+
end(handoff?: Handoff, reason?: WindowEndReason): Promise<void>;
|
|
101
|
+
/** Pause insertion without ending the window. */
|
|
102
|
+
pause(): Promise<void>;
|
|
103
|
+
resume(): Promise<void>;
|
|
104
|
+
usage(): TokenUsage;
|
|
105
|
+
/** Tool calls, results and appends as they happen. */
|
|
106
|
+
events(): AsyncIterable<WindowEvent>;
|
|
107
|
+
/** Resolves when the window ends, with why. */
|
|
108
|
+
outcome(): Promise<WindowOutcome>;
|
|
109
|
+
}
|
|
110
|
+
/** What a backend streams while a window runs. */
|
|
111
|
+
export type WindowEvent = {
|
|
112
|
+
kind: "tool_call";
|
|
113
|
+
call: ToolCall;
|
|
114
|
+
} | {
|
|
115
|
+
kind: "tool_result";
|
|
116
|
+
call: ToolCall;
|
|
117
|
+
result: ToolResult;
|
|
118
|
+
} | {
|
|
119
|
+
kind: "appended";
|
|
120
|
+
event: string;
|
|
121
|
+
} | {
|
|
122
|
+
kind: "steered";
|
|
123
|
+
delivery: Delivery;
|
|
124
|
+
} | {
|
|
125
|
+
kind: "usage";
|
|
126
|
+
usage: TokenUsage;
|
|
127
|
+
}
|
|
128
|
+
/** The run ended. The window stays open for a related input (docs/windows.md#lifecycle). */
|
|
129
|
+
| {
|
|
130
|
+
kind: "run_ended";
|
|
131
|
+
outcome: WindowOutcome;
|
|
132
|
+
} | {
|
|
133
|
+
kind: "ended";
|
|
134
|
+
outcome: WindowOutcome;
|
|
135
|
+
};
|
|
136
|
+
export type WindowEndReason = "model_ended" | "idle_timeout" | "hard_limit" | "host_restart" | "paused" | "failed";
|
|
137
|
+
export interface WindowOutcome {
|
|
138
|
+
window: WindowRef;
|
|
139
|
+
reason: WindowEndReason;
|
|
140
|
+
handoff?: Handoff;
|
|
141
|
+
usage: TokenUsage;
|
|
142
|
+
error?: string;
|
|
143
|
+
}
|
|
144
|
+
export interface AgentBackend {
|
|
145
|
+
readonly name: string;
|
|
146
|
+
/** The catalog the backend can run. Only models that take images are offered. */
|
|
147
|
+
models(): Promise<ModelInfo[]>;
|
|
148
|
+
login(request: LoginRequest): Promise<LoginResult>;
|
|
149
|
+
/**
|
|
150
|
+
* The backend's rate limits, when it reports any. There is no cap on windows;
|
|
151
|
+
* how many run at once is bounded by the backend, so new windows wait in a
|
|
152
|
+
* queue while it is limited (docs/dispatch.md#backpressure). A backend that
|
|
153
|
+
* reports nothing is never limited.
|
|
154
|
+
*/
|
|
155
|
+
rateLimits?(): Promise<RateLimitState> | RateLimitState;
|
|
156
|
+
/** Open one window and start its first turn. */
|
|
157
|
+
open(request: OpenWindowRequest): Promise<WindowSession>;
|
|
158
|
+
}
|
|
159
|
+
/** Declared to the model and called in the turn (docs/tools.md#direct-tools). */
|
|
160
|
+
export interface DirectToolSpec {
|
|
161
|
+
name: string;
|
|
162
|
+
/** What it does and when to use it; side effects. 1 to 4 sentences. */
|
|
163
|
+
description: string;
|
|
164
|
+
parameters: Record<string, ParameterSpec>;
|
|
165
|
+
}
|
|
166
|
+
/** Called from a script in `code_exec` (docs/tools.md#modules). */
|
|
167
|
+
export interface ModuleSpec {
|
|
168
|
+
name: string;
|
|
169
|
+
/** What the module can do, never a function name. */
|
|
170
|
+
description: string;
|
|
171
|
+
functions: readonly FunctionSpec[];
|
|
172
|
+
}
|
|
173
|
+
export interface FunctionSpec {
|
|
174
|
+
/** `module.function`. */
|
|
175
|
+
name: string;
|
|
176
|
+
signature: string;
|
|
177
|
+
description: string;
|
|
178
|
+
}
|
|
179
|
+
export interface ParameterSpec {
|
|
180
|
+
type: "string" | "number" | "boolean" | "ref" | "refs" | "blocks" | "object" | "array";
|
|
181
|
+
description: string;
|
|
182
|
+
required?: boolean;
|
|
183
|
+
default?: unknown;
|
|
184
|
+
}
|
|
185
|
+
/** One direct tool or module call, as the backend reports it. */
|
|
186
|
+
export interface ToolCall {
|
|
187
|
+
name: string;
|
|
188
|
+
args: Record<string, unknown>;
|
|
189
|
+
callId: string;
|
|
190
|
+
}
|
|
191
|
+
export interface ToolResult {
|
|
192
|
+
callId: string;
|
|
193
|
+
ok: boolean;
|
|
194
|
+
/** Stable fields, bounded, in the core's error shape when it failed. */
|
|
195
|
+
content: string;
|
|
196
|
+
error?: {
|
|
197
|
+
rule: string;
|
|
198
|
+
message: string;
|
|
199
|
+
nextAction?: string;
|
|
200
|
+
refs?: readonly Ref[];
|
|
201
|
+
};
|
|
202
|
+
}
|
package/agent.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
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).
|
|
6
|
+
*
|
|
7
|
+
* arbor uses pi-agent-core's `Agent` with its own system prompt: pi's default
|
|
8
|
+
* coding prompt, context files and skills never reach the model
|
|
9
|
+
* (docs/prompting.md#no-built-in-pi-prompt).
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
package/blocks.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed blocks and the text table (docs/prompting.md).
|
|
3
|
+
*
|
|
4
|
+
* No component writes prompt text. Everything that reaches a model - a system
|
|
5
|
+
* prompt section, a delivery, a reminder, a note, a result, an error - is a
|
|
6
|
+
* typed block: a kind and its fields. One renderer, owned by the host, turns
|
|
7
|
+
* blocks into text by fixed rules, so the same blocks always give the same
|
|
8
|
+
* bytes. A plugin registers kinds of its own and hands the host blocks, never
|
|
9
|
+
* finished text.
|
|
10
|
+
*/
|
|
11
|
+
import type { Ref } from "./refs.ts";
|
|
12
|
+
/** A field value inside a block. */
|
|
13
|
+
export type BlockValue = string | number | boolean | Ref | readonly Ref[] | readonly string[] | readonly Block[];
|
|
14
|
+
/** One typed block. */
|
|
15
|
+
export interface Block<K extends string = string> {
|
|
16
|
+
kind: K;
|
|
17
|
+
fields: Record<string, BlockValue>;
|
|
18
|
+
}
|
|
19
|
+
/** Built-in block kinds. Plugins add their own through `BlockKindSpec`. */
|
|
20
|
+
export type BlockKind = "task" | "board_line" | "handoff" | "note" | "note_summary" | "reminder" | "tool_result" | "error" | "data" | "outside_message" | (string & {});
|
|
21
|
+
/** Declares one field of a block kind. */
|
|
22
|
+
export interface BlockFieldSpec {
|
|
23
|
+
name: string;
|
|
24
|
+
type: "string" | "strings" | "number" | "boolean" | "ref" | "refs" | "blocks";
|
|
25
|
+
required?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A plugin's registration of a block kind: its fields, the tag the renderer
|
|
29
|
+
* wraps it in, and the fixed lines the renderer writes before its fields. The
|
|
30
|
+
* mapping is declarative so rendering stays deterministic and in one place.
|
|
31
|
+
*/
|
|
32
|
+
export interface BlockKindSpec {
|
|
33
|
+
kind: string;
|
|
34
|
+
/**
|
|
35
|
+
* A kind this one extends, built-in or another plugin's. Its tag, fields,
|
|
36
|
+
* lines and data mark are kept, and this spec's are added to them, so a
|
|
37
|
+
* plugin adds its own fields to a block the core builds - a note, an outside
|
|
38
|
+
* message - and the one renderer still renders it
|
|
39
|
+
* (docs/prompting.md#blocks-and-the-renderer).
|
|
40
|
+
*/
|
|
41
|
+
extends?: string;
|
|
42
|
+
/** XML tag the renderer wraps one block in; omitted for an untagged block. */
|
|
43
|
+
tag?: string;
|
|
44
|
+
fields: readonly BlockFieldSpec[];
|
|
45
|
+
/** Fixed lines written before the fields, by key into the text table. */
|
|
46
|
+
lines?: readonly string[];
|
|
47
|
+
/** Outside content is data, never instructions, and is marked where delivered. */
|
|
48
|
+
data?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One text table entry per fixed string the framework writes: section
|
|
52
|
+
* headings, delivery lines, reminders, descriptions, errors. A deployment
|
|
53
|
+
* replaces the table to change the language or the wording; no fixed text is
|
|
54
|
+
* written anywhere else. Identifiers stay as they are.
|
|
55
|
+
*/
|
|
56
|
+
export type TextTable = Record<string, string>;
|
package/blocks.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed blocks and the text table (docs/prompting.md).
|
|
3
|
+
*
|
|
4
|
+
* No component writes prompt text. Everything that reaches a model - a system
|
|
5
|
+
* prompt section, a delivery, a reminder, a note, a result, an error - is a
|
|
6
|
+
* typed block: a kind and its fields. One renderer, owned by the host, turns
|
|
7
|
+
* blocks into text by fixed rules, so the same blocks always give the same
|
|
8
|
+
* bytes. A plugin registers kinds of its own and hands the host blocks, never
|
|
9
|
+
* finished text.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
package/channel.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Channel` interface: an outside source and sink - look up a message by
|
|
3
|
+
* its native id, list what is connected, report online state
|
|
4
|
+
* (docs/architecture.md#interfaces).
|
|
5
|
+
*
|
|
6
|
+
* An outside message keeps its native id, with a short channel prefix:
|
|
7
|
+
* `qq:12345`. No second id is made up for it. When the channel is offline the
|
|
8
|
+
* lookup returns the stored copy and says the channel is offline; a channel
|
|
9
|
+
* plugged back in matches the same ids.
|
|
10
|
+
*/
|
|
11
|
+
import type { Block } from "./blocks.ts";
|
|
12
|
+
import type { ExternalRef, Ref, Target } from "./refs.ts";
|
|
13
|
+
/** One event from outside, before a task source turns it into a task. */
|
|
14
|
+
export interface OutsideEvent {
|
|
15
|
+
/** The channel or service it came from: `qq`, `svc:crawler`. */
|
|
16
|
+
source: string;
|
|
17
|
+
/** Its native id, with the channel prefix: `qq:12345`. */
|
|
18
|
+
external?: ExternalRef;
|
|
19
|
+
/** What it touches: the conversation, the issue, the service. */
|
|
20
|
+
target: Target;
|
|
21
|
+
time: number;
|
|
22
|
+
content: readonly Block[];
|
|
23
|
+
/** The outside message it answers, when it is a reply. */
|
|
24
|
+
replyTo?: ExternalRef;
|
|
25
|
+
refs?: readonly Ref[];
|
|
26
|
+
}
|
|
27
|
+
/** What a channel lookup returns: the stored copy, plus the channel's state. */
|
|
28
|
+
export interface OutsideMessage extends OutsideEvent {
|
|
29
|
+
external: ExternalRef;
|
|
30
|
+
/** True when the copy came from the journal rather than the live channel. */
|
|
31
|
+
stored: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface ChannelConnection {
|
|
34
|
+
id: string;
|
|
35
|
+
online: boolean;
|
|
36
|
+
detail?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface SendRequest {
|
|
39
|
+
channel: string;
|
|
40
|
+
/** The conversation or item it goes to. */
|
|
41
|
+
target: Target;
|
|
42
|
+
content: readonly Block[];
|
|
43
|
+
/** The outside message it answers, by native id. */
|
|
44
|
+
replyTo?: ExternalRef;
|
|
45
|
+
}
|
|
46
|
+
export interface SendResult {
|
|
47
|
+
external: ExternalRef;
|
|
48
|
+
time: number;
|
|
49
|
+
}
|
|
50
|
+
export type Unsubscribe = () => void | Promise<void>;
|
|
51
|
+
export interface Channel {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
/** What is connected, and whether each connection is online. */
|
|
54
|
+
connections(): Promise<readonly ChannelConnection[]>;
|
|
55
|
+
/** Look up a message by its native id. Returns the stored copy when offline. */
|
|
56
|
+
get(nativeId: ExternalRef): Promise<OutsideMessage | null>;
|
|
57
|
+
/** The sink: send out. Used by an effect executor, never by a window directly. */
|
|
58
|
+
send?(request: SendRequest): Promise<SendResult>;
|
|
59
|
+
/** Push source. A host that prefers polling may ignore this. */
|
|
60
|
+
subscribe?(handler: (events: readonly OutsideEvent[]) => void | Promise<void>): Unsubscribe;
|
|
61
|
+
}
|
package/channel.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Channel` interface: an outside source and sink - look up a message by
|
|
3
|
+
* its native id, list what is connected, report online state
|
|
4
|
+
* (docs/architecture.md#interfaces).
|
|
5
|
+
*
|
|
6
|
+
* An outside message keeps its native id, with a short channel prefix:
|
|
7
|
+
* `qq:12345`. No second id is made up for it. When the channel is offline the
|
|
8
|
+
* lookup returns the stored copy and says the channel is offline; a channel
|
|
9
|
+
* plugged back in matches the same ids.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
package/effects.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `EffectExecutor` interface and the effect queue's shapes
|
|
3
|
+
* (docs/architecture.md#interfaces, docs/coordination.md#effects).
|
|
4
|
+
*
|
|
5
|
+
* An effect takes place when it is made and cannot be rolled back. So every
|
|
6
|
+
* effect goes through one queue per host, which paces effects and executes
|
|
7
|
+
* each exactly once, in the order made; effects on one target are serial. Each
|
|
8
|
+
* effect is recorded as an event, so continuations and restarts never repeat
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
import type { Block } from "./blocks.ts";
|
|
12
|
+
import type { ExternalRef, Ref, Target, WindowRef } from "./refs.ts";
|
|
13
|
+
import type { ArborErrorInfo } from "@4fu/arbor-errors";
|
|
14
|
+
export type EffectKind = "send" | (string & {});
|
|
15
|
+
/** One queued effect, as the host hands it to an executor. */
|
|
16
|
+
export interface Effect {
|
|
17
|
+
/** The event that recorded it, so a continuation never repeats it. */
|
|
18
|
+
event: Ref;
|
|
19
|
+
kind: EffectKind;
|
|
20
|
+
target: Target;
|
|
21
|
+
/** The outside address, for a send. */
|
|
22
|
+
to?: string;
|
|
23
|
+
content: readonly Block[];
|
|
24
|
+
/** The outside message it answers. */
|
|
25
|
+
replyTo?: ExternalRef;
|
|
26
|
+
cites?: readonly Ref[];
|
|
27
|
+
madeBy: WindowRef;
|
|
28
|
+
time: number;
|
|
29
|
+
}
|
|
30
|
+
export interface EffectResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
/** The native id the outside world gave it, for the event's `external`. */
|
|
33
|
+
external?: ExternalRef;
|
|
34
|
+
error?: ArborErrorInfo;
|
|
35
|
+
}
|
|
36
|
+
export interface EffectExecutor {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
/** Whether this executor owns the effect. One that does not refuses it. */
|
|
39
|
+
can(effect: Effect): boolean;
|
|
40
|
+
/** Carry the effect out. Called once per effect, by the queue. */
|
|
41
|
+
run(effect: Effect): Promise<EffectResult>;
|
|
42
|
+
}
|
package/effects.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `EffectExecutor` interface and the effect queue's shapes
|
|
3
|
+
* (docs/architecture.md#interfaces, docs/coordination.md#effects).
|
|
4
|
+
*
|
|
5
|
+
* An effect takes place when it is made and cannot be rolled back. So every
|
|
6
|
+
* effect goes through one queue per host, which paces effects and executes
|
|
7
|
+
* each exactly once, in the order made; effects on one target are serial. Each
|
|
8
|
+
* effect is recorded as an event, so continuations and restarts never repeat
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|