@4fu/arbor-judge-jev 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +187 -2
- package/bounds.d.ts +68 -0
- package/bounds.js +116 -0
- package/index.d.ts +19 -0
- package/index.js +17 -0
- package/jev.d.ts +62 -0
- package/jev.js +233 -0
- package/options.d.ts +160 -0
- package/options.js +63 -0
- package/package.json +44 -4
- package/questions.d.ts +99 -0
- package/questions.js +418 -0
- package/scrub.d.ts +17 -0
- package/scrub.js +35 -0
- package/wire.d.ts +86 -0
- package/wire.js +103 -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/bounds.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounds on what one judge request carries.
|
|
3
|
+
*
|
|
4
|
+
* Jev's context window is 64k tokens, and the state is chat text, window
|
|
5
|
+
* profiles and note summaries: a host with a long task or many live windows
|
|
6
|
+
* must not post all of it. The bounds here are the judge's own, computed in
|
|
7
|
+
* code, and every cut is reported in the state as a labelled fact
|
|
8
|
+
* (`dropped_content`, `dropped_candidates`, `dropped_older`) so a question
|
|
9
|
+
* never reads a silence that is only a cut - the rule a shared recent context
|
|
10
|
+
* states the same way.
|
|
11
|
+
*
|
|
12
|
+
* Nothing is cut in half. A list whose newest items matter most - a task's
|
|
13
|
+
* content blocks - keeps the newest whole items that fit; a list whose leading
|
|
14
|
+
* items are already the strongest - the host's candidate order, or the rule
|
|
15
|
+
* score - keeps the leading ones. Either way the newest item is kept even when
|
|
16
|
+
* it alone is bigger than the budget, because a decision with no items at all
|
|
17
|
+
* answers nothing.
|
|
18
|
+
*/
|
|
19
|
+
/** Tokens one task's content blocks may use. */
|
|
20
|
+
export declare const CONTENT_TOKENS = 8000;
|
|
21
|
+
/** Tokens the candidate windows of one decision may use. */
|
|
22
|
+
export declare const CANDIDATES_TOKENS = 6000;
|
|
23
|
+
/** Characters one string in the state may use. */
|
|
24
|
+
export declare const MAX_TEXT_CHARS = 400;
|
|
25
|
+
/** How many of a window's taken tasks the state carries. */
|
|
26
|
+
export declare const MAX_TASKS = 5;
|
|
27
|
+
/** How many of a window's last inputs the state carries. */
|
|
28
|
+
export declare const MAX_LAST_INPUTS = 3;
|
|
29
|
+
/** How many of a window's expectations the state carries. */
|
|
30
|
+
export declare const MAX_EXPECTS = 8;
|
|
31
|
+
/**
|
|
32
|
+
* A rough token estimate: a Chinese, Japanese or Korean character is one
|
|
33
|
+
* token, and every other script about four characters. It is an estimate, and
|
|
34
|
+
* it is the same one for every item, which is what a budget needs.
|
|
35
|
+
*/
|
|
36
|
+
export declare function estimateTokens(text: string): number;
|
|
37
|
+
/** What one item costs, as its JSON form. */
|
|
38
|
+
export declare function jsonCost(item: unknown): number;
|
|
39
|
+
/**
|
|
40
|
+
* The newest whole items that fit the budget, oldest first, with how many older
|
|
41
|
+
* ones were left out. An item is never cut in half, and the newest item is kept
|
|
42
|
+
* even when it alone is bigger than the budget, because a task with no content
|
|
43
|
+
* at all answers nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fitNewest<T>(items: readonly T[], cost: (item: T) => number, budget: number): {
|
|
46
|
+
kept: T[];
|
|
47
|
+
dropped: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The leading whole items that fit the budget, with how many later ones were
|
|
51
|
+
* left out. Used where the caller's order is already a ranking - the host's
|
|
52
|
+
* candidate order, or the rule score - so the strongest items are the ones
|
|
53
|
+
* asked about and the rest are reported as dropped.
|
|
54
|
+
*/
|
|
55
|
+
export declare function fit<T>(items: readonly T[], cost: (item: T) => number, budget: number): {
|
|
56
|
+
kept: T[];
|
|
57
|
+
dropped: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* One string cut at `limit` characters, counted in code points so a character
|
|
61
|
+
* is never split, with a marker that says it was cut.
|
|
62
|
+
*/
|
|
63
|
+
export declare function truncate(text: string, limit?: number): string;
|
|
64
|
+
/** The newest `limit` items of a list, with how many older ones were left out. */
|
|
65
|
+
export declare function newest<T>(items: readonly T[], limit: number): {
|
|
66
|
+
kept: T[];
|
|
67
|
+
dropped: number;
|
|
68
|
+
};
|
package/bounds.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bounds on what one judge request carries.
|
|
3
|
+
*
|
|
4
|
+
* Jev's context window is 64k tokens, and the state is chat text, window
|
|
5
|
+
* profiles and note summaries: a host with a long task or many live windows
|
|
6
|
+
* must not post all of it. The bounds here are the judge's own, computed in
|
|
7
|
+
* code, and every cut is reported in the state as a labelled fact
|
|
8
|
+
* (`dropped_content`, `dropped_candidates`, `dropped_older`) so a question
|
|
9
|
+
* never reads a silence that is only a cut - the rule a shared recent context
|
|
10
|
+
* states the same way.
|
|
11
|
+
*
|
|
12
|
+
* Nothing is cut in half. A list whose newest items matter most - a task's
|
|
13
|
+
* content blocks - keeps the newest whole items that fit; a list whose leading
|
|
14
|
+
* items are already the strongest - the host's candidate order, or the rule
|
|
15
|
+
* score - keeps the leading ones. Either way the newest item is kept even when
|
|
16
|
+
* it alone is bigger than the budget, because a decision with no items at all
|
|
17
|
+
* answers nothing.
|
|
18
|
+
*/
|
|
19
|
+
/** Tokens one task's content blocks may use. */
|
|
20
|
+
export const CONTENT_TOKENS = 8_000;
|
|
21
|
+
/** Tokens the candidate windows of one decision may use. */
|
|
22
|
+
export const CANDIDATES_TOKENS = 6_000;
|
|
23
|
+
/** Characters one string in the state may use. */
|
|
24
|
+
export const MAX_TEXT_CHARS = 400;
|
|
25
|
+
/** How many of a window's taken tasks the state carries. */
|
|
26
|
+
export const MAX_TASKS = 5;
|
|
27
|
+
/** How many of a window's last inputs the state carries. */
|
|
28
|
+
export const MAX_LAST_INPUTS = 3;
|
|
29
|
+
/** How many of a window's expectations the state carries. */
|
|
30
|
+
export const MAX_EXPECTS = 8;
|
|
31
|
+
/**
|
|
32
|
+
* A rough token estimate: a Chinese, Japanese or Korean character is one
|
|
33
|
+
* token, and every other script about four characters. It is an estimate, and
|
|
34
|
+
* it is the same one for every item, which is what a budget needs.
|
|
35
|
+
*/
|
|
36
|
+
export function estimateTokens(text) {
|
|
37
|
+
let tokens = 0;
|
|
38
|
+
let other = 0;
|
|
39
|
+
for (const character of text) {
|
|
40
|
+
if (isCjk(character.codePointAt(0) ?? 0)) {
|
|
41
|
+
tokens += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
other += 1;
|
|
45
|
+
if (other === 4) {
|
|
46
|
+
tokens += 1;
|
|
47
|
+
other = 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return tokens + (other > 0 ? 1 : 0);
|
|
51
|
+
}
|
|
52
|
+
function isCjk(codePoint) {
|
|
53
|
+
return ((codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
|
|
54
|
+
(codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
|
|
55
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
56
|
+
(codePoint >= 0x3040 && codePoint <= 0x30ff) ||
|
|
57
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7af) ||
|
|
58
|
+
(codePoint >= 0x20000 && codePoint <= 0x2a6df) ||
|
|
59
|
+
(codePoint >= 0x2a700 && codePoint <= 0x2ebef));
|
|
60
|
+
}
|
|
61
|
+
/** What one item costs, as its JSON form. */
|
|
62
|
+
export function jsonCost(item) {
|
|
63
|
+
return estimateTokens(JSON.stringify(item) ?? "");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The newest whole items that fit the budget, oldest first, with how many older
|
|
67
|
+
* ones were left out. An item is never cut in half, and the newest item is kept
|
|
68
|
+
* even when it alone is bigger than the budget, because a task with no content
|
|
69
|
+
* at all answers nothing.
|
|
70
|
+
*/
|
|
71
|
+
export function fitNewest(items, cost, budget) {
|
|
72
|
+
const kept = [];
|
|
73
|
+
let used = 0;
|
|
74
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
75
|
+
const item = items[index];
|
|
76
|
+
const size = cost(item);
|
|
77
|
+
if (kept.length > 0 && used + size > budget)
|
|
78
|
+
break;
|
|
79
|
+
kept.push(item);
|
|
80
|
+
used += size;
|
|
81
|
+
}
|
|
82
|
+
kept.reverse();
|
|
83
|
+
return { kept, dropped: items.length - kept.length };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The leading whole items that fit the budget, with how many later ones were
|
|
87
|
+
* left out. Used where the caller's order is already a ranking - the host's
|
|
88
|
+
* candidate order, or the rule score - so the strongest items are the ones
|
|
89
|
+
* asked about and the rest are reported as dropped.
|
|
90
|
+
*/
|
|
91
|
+
export function fit(items, cost, budget) {
|
|
92
|
+
const kept = [];
|
|
93
|
+
let used = 0;
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
const size = cost(item);
|
|
96
|
+
if (kept.length > 0 && used + size > budget)
|
|
97
|
+
break;
|
|
98
|
+
kept.push(item);
|
|
99
|
+
used += size;
|
|
100
|
+
}
|
|
101
|
+
return { kept, dropped: items.length - kept.length };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* One string cut at `limit` characters, counted in code points so a character
|
|
105
|
+
* is never split, with a marker that says it was cut.
|
|
106
|
+
*/
|
|
107
|
+
export function truncate(text, limit = MAX_TEXT_CHARS) {
|
|
108
|
+
const characters = Array.from(text);
|
|
109
|
+
if (characters.length <= limit)
|
|
110
|
+
return text;
|
|
111
|
+
return `${characters.slice(0, limit).join("")}…`;
|
|
112
|
+
}
|
|
113
|
+
/** The newest `limit` items of a list, with how many older ones were left out. */
|
|
114
|
+
export function newest(items, limit) {
|
|
115
|
+
return { kept: items.slice(0, limit), dropped: Math.max(0, items.length - limit) };
|
|
116
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @4fu/arbor-judge-jev - the default `Judge`.
|
|
3
|
+
*
|
|
4
|
+
* Jev is TypeSafe AI's System One classifier: one request carries one state and
|
|
5
|
+
* several typed questions about it, and every answer comes back with its score
|
|
6
|
+
* or probability. It answers the host's decisions - filter, route, model tier,
|
|
7
|
+
* notice, broadcast, active set (docs/dispatch.md#judge-decisions) - one request
|
|
8
|
+
* per decision, scrubbed of secrets on the way out, and every answer logged
|
|
9
|
+
* with its scores.
|
|
10
|
+
*
|
|
11
|
+
* The configuration names the secret (`JEV_API_KEY`) and never holds its value,
|
|
12
|
+
* and a fake `fetchImpl` is injected in tests, so the whole contract runs
|
|
13
|
+
* without a key and without the network.
|
|
14
|
+
*/
|
|
15
|
+
export { Jev, isJevUnavailable } from "./jev.ts";
|
|
16
|
+
export { JEV_API_KEY_ENV, JEV_BASE_URL, JEV_MODEL_ID, DEFAULT_MAX_ITEMS, DEFAULT_TIMEOUT_MS, DEFAULT_TIER_CRITERIA, resolveJevOptions, } from "./options.ts";
|
|
17
|
+
export type { JevOptions, JevThresholds, JevDecisionLog, ResolvedJevOptions, BoolQuestion, ScoreQuestion, ChoiceQuestion, JudgeQuestion, JudgeAnswerValue, } from "./options.ts";
|
|
18
|
+
export type { WireRequest, WireResponse } from "./wire.ts";
|
|
19
|
+
export { scrubJson } from "./scrub.ts";
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @4fu/arbor-judge-jev - the default `Judge`.
|
|
3
|
+
*
|
|
4
|
+
* Jev is TypeSafe AI's System One classifier: one request carries one state and
|
|
5
|
+
* several typed questions about it, and every answer comes back with its score
|
|
6
|
+
* or probability. It answers the host's decisions - filter, route, model tier,
|
|
7
|
+
* notice, broadcast, active set (docs/dispatch.md#judge-decisions) - one request
|
|
8
|
+
* per decision, scrubbed of secrets on the way out, and every answer logged
|
|
9
|
+
* with its scores.
|
|
10
|
+
*
|
|
11
|
+
* The configuration names the secret (`JEV_API_KEY`) and never holds its value,
|
|
12
|
+
* and a fake `fetchImpl` is injected in tests, so the whole contract runs
|
|
13
|
+
* without a key and without the network.
|
|
14
|
+
*/
|
|
15
|
+
export { Jev, isJevUnavailable } from "./jev.js";
|
|
16
|
+
export { JEV_API_KEY_ENV, JEV_BASE_URL, JEV_MODEL_ID, DEFAULT_MAX_ITEMS, DEFAULT_TIMEOUT_MS, DEFAULT_TIER_CRITERIA, resolveJevOptions, } from "./options.js";
|
|
17
|
+
export { scrubJson } from "./scrub.js";
|
package/jev.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jev, the default `Judge` (docs/dispatch.md#judge-decisions).
|
|
3
|
+
*
|
|
4
|
+
* Jev is TypeSafe AI's System One classifier: `POST {base}/systemone` with one
|
|
5
|
+
* JSON state and several typed questions about it, and one answer per question,
|
|
6
|
+
* each with its score or probability. It answers the host's decisions - filter,
|
|
7
|
+
* route, model tier, notice, broadcast, active set - one request per decision
|
|
8
|
+
* with its questions batched, and it decides whether, where and when. It never
|
|
9
|
+
* writes text; the model owns content.
|
|
10
|
+
*
|
|
11
|
+
* This wrapper owns four things and nothing else:
|
|
12
|
+
*
|
|
13
|
+
* 1. **One request per decision.** `decide` builds the decision's questions
|
|
14
|
+
* and its state (questions.ts), scrubs both (scrub.ts), posts them once
|
|
15
|
+
* (wire.ts) and reads the answers back into the core's own shapes.
|
|
16
|
+
* 2. **Scrubbing on the way out.** Every string of the state and of the
|
|
17
|
+
* questions goes through `scrub` before it leaves the host, and the decision
|
|
18
|
+
* log records the scrubbed form, so a secret value reaches neither Jev nor
|
|
19
|
+
* the log (docs/dispatch.md#judge-decisions).
|
|
20
|
+
* 3. **Every answer with its scores.** `onDecision` is called once per decision
|
|
21
|
+
* with the state the judge saw, the questions, every answer with its score
|
|
22
|
+
* or probability, the token usage and how long it took, so the host logs
|
|
23
|
+
* them and tunes its thresholds from real decisions.
|
|
24
|
+
* 4. **Refusing, not guessing.** A missing key, a transport failure or an
|
|
25
|
+
* answer of the wrong shape raises the core's `unavailable` error naming the
|
|
26
|
+
* rule and the next action. It never fabricates an answer: the rule bounds
|
|
27
|
+
* and the fallbacks live in the service layer, so an unreachable judge
|
|
28
|
+
* changes nothing about the rules (packages/interfaces/src/judge.ts).
|
|
29
|
+
*
|
|
30
|
+
* What it does not own: the rule bounds and the fallbacks around a decision.
|
|
31
|
+
* The judge answers what it is asked, in `shadow` exactly as in `on`, and the
|
|
32
|
+
* mode rides on the decision log. A decision that is `off` asks nothing at all
|
|
33
|
+
* and is answered with that decision's own documented default
|
|
34
|
+
* (docs/dispatch.md#judge-decisions), so it costs no request.
|
|
35
|
+
*/
|
|
36
|
+
import type { Judge, JudgeAnswer, JudgeRequest } from "@4fu/arbor-interfaces";
|
|
37
|
+
import type { JevOptions } from "./options.ts";
|
|
38
|
+
export declare class Jev implements Judge {
|
|
39
|
+
readonly name = "jev";
|
|
40
|
+
private readonly options;
|
|
41
|
+
constructor(options?: JevOptions);
|
|
42
|
+
/** Answer one decision. One request, with that decision's questions batched. */
|
|
43
|
+
decide(request: JudgeRequest): Promise<JudgeAnswer>;
|
|
44
|
+
private filter;
|
|
45
|
+
private route;
|
|
46
|
+
private modelTier;
|
|
47
|
+
private notice;
|
|
48
|
+
private broadcast;
|
|
49
|
+
private activeSet;
|
|
50
|
+
/**
|
|
51
|
+
* Posts one decision and reads its answers back. The state and the questions
|
|
52
|
+
* are scrubbed once, here, on the way out, and the scrubbed forms are what the
|
|
53
|
+
* decision log records.
|
|
54
|
+
*/
|
|
55
|
+
private run;
|
|
56
|
+
/** One System One request, with the bearer key read per call. */
|
|
57
|
+
private post;
|
|
58
|
+
/** The decision log record, handed to the host for its own journal. */
|
|
59
|
+
private log;
|
|
60
|
+
}
|
|
61
|
+
/** True when an error is a refusal of this package: the judge could not answer. */
|
|
62
|
+
export declare function isJevUnavailable(error: unknown): boolean;
|