@4fu/arbor-interfaces 2026.1106.1 → 2026.1107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,9 @@ npm install @4fu/arbor-core @4fu/arbor-journal-sqlite @4fu/arbor-notes-sqlite \
56
56
  ```
57
57
 
58
58
  `@4fu/arbor-plugin-http` is optional; it is the example plugin below.
59
- `@4fu/arbor-segmenter-jieba` is an optional peer of the journal.
59
+ `@4fu/arbor-segmenter-jieba` is an optional peer of the lexical package, and
60
+ `@4fu/arbor-search-zvec` an optional peer of the journal: absent either one, the
61
+ journal searches lexically and nothing else changes.
60
62
 
61
63
  ## A minimal host
62
64
 
@@ -120,6 +122,8 @@ window gets, and how a continuation keeps working in it, are in
120
122
  | [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
121
123
  | [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
122
124
  | [`@4fu/arbor-segmenter-jieba`](packages/segmenter-jieba) | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
125
+ | [`@4fu/arbor-lexical`](packages/lexical) | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
126
+ | [`@4fu/arbor-search-zvec`](packages/search-zvec) | the optional semantic channel: event text embedded and searched by meaning with zvec |
123
127
  | [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the fallback `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
124
128
  | [`@4fu/arbor-workspace-ovnfs`](packages/workspace-ovnfs) | the default `Workspace` when ovnfs is installed: one shared folder, per-window copy-on-write checkouts the model opens and applies back |
125
129
  | [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
@@ -171,6 +175,7 @@ tarball would contain ([docs/release.md](docs/release.md)).
171
175
  | `packages/errors` | the error shape every refusal uses: the rule and the next action |
172
176
  | `packages/journal-sqlite` | the default `Journal` and `BlobStore` on SQLite |
173
177
  | `packages/segmenter-jieba` | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
178
+ | `packages/lexical` | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
174
179
  | `packages/notes-sqlite` | the default `NoteStore` on SQLite, beside the journal |
175
180
  | `packages/judge-jev` | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
176
181
  | `packages/workspace-dir` | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place, and the lookup of another window's directory it contributes to the catalog |
package/agent.d.ts CHANGED
@@ -13,6 +13,17 @@ import type { ExternalTool } from "./tools.ts";
13
13
  import type { TaskRef, Ref, WindowRef } from "./refs.ts";
14
14
  import type { Handoff, TokenUsage, WindowState } from "./windows.ts";
15
15
  import type { RateLimitState } from "./scheduler.ts";
16
+ /**
17
+ * One JSON Schema object, carried as it was declared.
18
+ *
19
+ * A tool declared from outside the core - a plugin's own, or a harness's
20
+ * through a backend - hands its parameters over in the JSON Schema that
21
+ * harness already speaks, and that schema is what the model reads, verbatim
22
+ * (docs/tools.md#external-tools). Deriving one from a summary would drop what
23
+ * the schema says and what the summary cannot: `enum`, `items`, `oneOf`,
24
+ * `additionalProperties`, a nested object's own properties.
25
+ */
26
+ export type JSONSchema = Record<string, unknown>;
16
27
  /** One model the backend can run. Only models that take images are offered. */
17
28
  export interface ModelInfo {
18
29
  id: string;
@@ -78,7 +89,32 @@ export interface ContinuationInfo {
78
89
  /** Set after the hard limit: the window was reset mid-task. */
79
90
  midTaskReset?: boolean;
80
91
  }
81
- /** What the host inserts into a running window at a tool-call boundary. */
92
+ /**
93
+ * What the host inserts into a running window at a tool-call boundary.
94
+ *
95
+ * ## What each kind expects from a backend
96
+ *
97
+ * Every delivery is one input block, and the host says when the model must
98
+ * read it by the call it hands the delivery to: `WindowSession.steer` for one
99
+ * that goes in at the next step boundary, `WindowSession.followUp` for one that
100
+ * becomes the input of the next turn. The rule is the same for every kind - the
101
+ * insertion rules decide, not the kind
102
+ * (docs/dispatch.md#insertion-timing) - with two things a backend can rely on:
103
+ *
104
+ * - **What a window can call is never held back.** A conversation update, an
105
+ * environment delta, a prompt delta and a tools delta always come through
106
+ * `steer`, in the protection period and after it, because they change what
107
+ * the window would otherwise do next.
108
+ * - **A reminder is the kind the host times itself.** While the window runs it
109
+ * is steered in at the next boundary; once the run has ended it is queued as
110
+ * a follow-up, so it is the input of the window's next turn.
111
+ *
112
+ * A backend that is not pi maps the two calls onto whatever its own loop
113
+ * offers - DeepSeek Harness's `agent.inject` for a step boundary and its
114
+ * steering for the next turn - and the rest of the contract is the same: the
115
+ * block is one user-role input, in order, exactly once, and the turn the
116
+ * window is in is never ended by it.
117
+ */
82
118
  export interface Delivery {
83
119
  kind: "inserted_task" | "conversation_update" | "notice" | "broadcast" | "window_message" | "environment_delta"
84
120
  /**
@@ -87,6 +123,13 @@ export interface Delivery {
87
123
  * (docs/prompting.md#plugin-sections-that-change).
88
124
  */
89
125
  | "prompt_delta"
126
+ /**
127
+ * The direct tools declared from outside the core changed while this window
128
+ * runs: a harness registered one, or a plugin's set moved. The delta names
129
+ * what was added and what was removed, and the window may call the new ones
130
+ * from its next tool call on (docs/tools.md#external-tools-that-change).
131
+ */
132
+ | "tools_delta"
90
133
  /** A host usage reminder, appended to an input (docs/windows.md#usage-reminders). */
91
134
  | "reminder";
92
135
  /** The window it comes from, for a notice, a broadcast or a window message. */
@@ -99,10 +142,66 @@ export interface Delivery {
99
142
  export interface WindowSession {
100
143
  readonly window: WindowRef;
101
144
  readonly state: WindowState;
102
- /** Insert at the next tool-call boundary, through the backend's steering. */
145
+ /**
146
+ * Insert at the next tool-call boundary, through the backend's steering.
147
+ *
148
+ * A **step boundary** is the moment every tool call of the current turn has
149
+ * finished and the next model request has not gone out yet. The delivery
150
+ * becomes one user-role input the model reads with that request. It is never
151
+ * a system message in the middle of a window - some providers fold those into
152
+ * the leading system prompt and that breaks the cached prefix
153
+ * (docs/prompting.md#no-built-in-pi-prompt) - and it never rewrites the
154
+ * window's prefix.
155
+ *
156
+ * What a backend must do:
157
+ *
158
+ * - deliver the block in the order the host handed its deliveries over, and
159
+ * deliver each one exactly once;
160
+ * - keep the turn going: the model is not stopped, not asked to confirm, and
161
+ * not told the input is special - it is one more input;
162
+ * - start a turn when the window is idle, because that is what a related
163
+ * input wants (docs/dispatch.md#insertion-timing); the host only steers
164
+ * into a window it has not ended and has not paused, so a delivery never
165
+ * arrives for one of those;
166
+ * - resolve once the delivery is queued, not once the model has read it: the
167
+ * host never waits for the model, and a slow model is not a slow steer.
168
+ *
169
+ * A window that has ended refuses in the core's error shape, rule
170
+ * `window.ended`; the host never steers into one.
171
+ */
103
172
  steer(delivery: Delivery): Promise<void>;
104
- /** Queue for after the run ends. */
173
+ /**
174
+ * Queue for after the run ends: the delivery becomes the input of the
175
+ * window's next turn.
176
+ *
177
+ * What a backend must do:
178
+ *
179
+ * - hold it out of the turn the window is in - the model must not read it
180
+ * mid-turn, which is what separates this call from `steer`;
181
+ * - deliver it as the input of the next turn, in order and exactly once, when
182
+ * the window runs again;
183
+ * - keep it queued while the window sits idle, and drop it when the window
184
+ * ends, because an ended window is never reopened
185
+ * (docs/windows.md#lifecycle).
186
+ *
187
+ * The host uses this only for a delivery that belongs after the run: the
188
+ * reminder of a window whose run has already ended, and whatever a
189
+ * `Scheduler` places there (docs/dispatch.md#insertion-timing).
190
+ */
105
191
  followUp(delivery: Delivery): Promise<void>;
192
+ /**
193
+ * The direct tools this window may call changed: the host registered what a
194
+ * backend or a plugin declared since this window opened
195
+ * (docs/tools.md#external-tools-that-change).
196
+ *
197
+ * A backend that can change a running window's declared tools replaces them
198
+ * here, so a tool that appeared is callable in this window and one that was
199
+ * removed is not. A backend that cannot leaves the declaration it opened
200
+ * with: the window still learns the change from the `tools_delta` it is
201
+ * steered, and a call still runs through the host's catalog, which holds the
202
+ * current set. Optional; the host asks only of a session that has it.
203
+ */
204
+ refreshTools?(tools: readonly DirectToolSpec[]): Promise<void> | void;
106
205
  /** End the run. With a handoff the host opens a continuation. */
107
206
  end(handoff?: Handoff, reason?: WindowEndReason): Promise<void>;
108
207
  /** Pause insertion without ending the window. */
@@ -191,8 +290,14 @@ export interface AgentBackend {
191
290
  * Direct tools the backend injects from outside arbor: the tools an outside
192
291
  * harness registered, which a host embedded in that harness hands in here
193
292
  * (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.
293
+ * in the turn like any other direct tool.
294
+ *
295
+ * The set is refreshable: the host reads it again before every window opens
296
+ * and at every step boundary, so a tool the harness registers - or drops -
297
+ * while the host runs reaches the model without a restart
298
+ * (docs/tools.md#external-tools-that-change). A backend that wants the change
299
+ * to land the moment it happens rather than at the next boundary calls
300
+ * `Arbor.refreshExternalTools` from its own signal.
196
301
  */
197
302
  externalTools?(): readonly ExternalTool[] | Promise<readonly ExternalTool[]>;
198
303
  /** Open one window and start its first turn. */
@@ -203,7 +308,17 @@ export interface DirectToolSpec {
203
308
  name: string;
204
309
  /** What it does and when to use it; side effects. 1 to 4 sentences. */
205
310
  description: string;
311
+ /** The parameters in arbor's own declaration shape. */
206
312
  parameters: Record<string, ParameterSpec>;
313
+ /**
314
+ * The parameters as the JSON Schema an outside harness declared them, carried
315
+ * verbatim: `enum`, `items`, `oneOf`, `additionalProperties`, a nested
316
+ * object's own properties, all of it. What the model reads when the tool has
317
+ * one, and what the `<tools>` line and `tool_search` show
318
+ * (docs/tools.md#external-tools). Arbor's own tools declare none: their
319
+ * `parameters` are the declaration.
320
+ */
321
+ schema?: JSONSchema;
207
322
  /**
208
323
  * Who declares it, when it is not the core's own: a plugin's name, or a
209
324
  * backend's. The tools list says where such a tool comes from, so the model
package/events.d.ts CHANGED
@@ -108,7 +108,11 @@ export interface SearchHit {
108
108
  kind: EventKind;
109
109
  time: number;
110
110
  match: MatchKind;
111
- /** Relevance in 0..1 within its index; higher is better. */
111
+ /**
112
+ * Relevance in 0..1 across the channels that found the hit; higher is better.
113
+ * Fused with reciprocal rank fusion, so a hit two channels found outranks one
114
+ * only one did.
115
+ */
112
116
  score: number;
113
117
  /** The event's opening words, for a one-line result. */
114
118
  summary: string;
package/index.d.ts CHANGED
@@ -11,8 +11,8 @@ export type { BlockValue, Block, BlockKind, BlockFieldSpec, BlockKindSpec, TextT
11
11
  /** Page bounds every read reports (docs/prompting.md). */
12
12
  export { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT } from "./refs.ts";
13
13
  export type { EventInput, Link, LinkInput, Redaction, Event, ListOptions, Page, EventPage, AroundOptions, SearchOptions, SearchHit, SearchPage, LinksOptions, RelatedItem, LinkPage, TraceOptions, TraceNode, TargetGap, JournalError, } from "./events.ts";
14
- export type { BlobInput, BlobContent, BlobInfo, BlobStore, Segmenter, Journal, } from "./journal.ts";
15
- export type { ModelInfo, LoginState, LoginRequest, LoginResult, RenderedPrompt, OpenWindowRequest, WindowLimits, ContinuationInfo, Delivery, WindowSession, StreamEvent, WindowEvent, WindowEndReason, WindowOutcome, AgentBackend, DirectToolSpec, ModuleSpec, FunctionSpec, ParameterSpec, ToolCall, ToolResult, } from "./agent.ts";
14
+ export type { BlobInput, BlobContent, BlobInfo, BlobStore, Segmenter, SemanticFilters, SemanticFields, Embedder, SemanticIndex, Journal, } from "./journal.ts";
15
+ export type { ModelInfo, LoginState, LoginRequest, LoginResult, RenderedPrompt, OpenWindowRequest, WindowLimits, ContinuationInfo, Delivery, WindowSession, StreamEvent, WindowEvent, WindowEndReason, WindowOutcome, AgentBackend, DirectToolSpec, ModuleSpec, FunctionSpec, ParameterSpec, JSONSchema, ToolCall, ToolResult, } from "./agent.ts";
16
16
  export type { WindowState, WindowRuntimeState, TokenUsage, Handoff, BoardLine, Expectation, InputBlock, } from "./windows.ts";
17
17
  export type { NoteStatus, Anchor, NoteWriteInput, Note, NoteSummary, NoteBody, NoteRevision, NoteContext, NoteQuery, NoteConflictInfo, ActiveSetBudget, ActiveCandidate, NoteUseKind, NoteUseInput, SnapshotOptions, NotesSnapshot, NoteDelta, NoteDeltaItem, NoteStore, } from "./notes.ts";
18
18
  export type { ExecResult, StagedFile, OpenWorkItem, WorkspaceCheckout, WorkspaceKeep, WorkspaceMount, WorkspaceQuota, Workspace, } from "./workspace.ts";
@@ -20,7 +20,7 @@ export type { JudgeMode, JudgeDecisionKind, CandidateWindow, JudgeTask, FilterSt
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, ExternalTool } from "./tools.ts";
23
+ export type { CallContext, ToolGroup, ExternalTool, ExternalToolSource, ExternalToolChange, } from "./tools.ts";
24
24
  export type { EnvItemKind, EnvItem, EnvDraft, EnvValidation, EnvRevision, EnvDelta, Environment, } from "./environment.ts";
25
25
  export type { ServiceState, ServiceSpec, ServiceHandle, ServiceStatus, LogOptions, LogPage, Supervisor, } from "./supervisor.ts";
26
26
  export type { QueuedInput, InsertionRequest, InsertionPlan, RateLimitState, AdmissionRequest, AdmissionPlan, SkipPeriod, DailyRepeat, EveryRepeat, RepeatBounds, RepeatRule, WeeklyRepeat, ScheduledTaskInput, ScheduledTask, ScheduledTaskUpdate, Scheduler, } from "./scheduler.ts";
package/journal.d.ts CHANGED
@@ -52,6 +52,60 @@ export interface BlobStore {
52
52
  export interface Segmenter {
53
53
  segment(text: string): string[];
54
54
  }
55
+ /** The filters a semantic search applies, as the journal's own read applies them. */
56
+ export interface SemanticFilters {
57
+ kinds?: readonly string[];
58
+ window?: WindowRef | null;
59
+ afterSeq?: number;
60
+ beforeSeq?: number;
61
+ }
62
+ /** The scalar fields a semantic filter reads, beside an event's text. */
63
+ export interface SemanticFields {
64
+ kind?: string;
65
+ window?: WindowRef | null;
66
+ }
67
+ /**
68
+ * Embedding text into vectors. The default is a small local model that needs no
69
+ * key (docs/journal.md#search); a deployment replaces it with its own, which is
70
+ * the only way text reaches anything outside the host.
71
+ */
72
+ export interface Embedder {
73
+ /** The dimension every vector this embedder returns has. */
74
+ readonly dimension: number;
75
+ /** Embed each text, in the order given. */
76
+ embed(texts: readonly string[]): Promise<readonly number[][]>;
77
+ }
78
+ /**
79
+ * The semantic channel: text search by meaning, beside the lexical indexes
80
+ * (docs/journal.md#search, docs/memory.md#search).
81
+ *
82
+ * A store takes one as an optional peer, the way it takes a `Segmenter`.
83
+ * When none is registered - the package is not installed, its vector store will
84
+ * not open on the platform, or no embedding model is available - search is
85
+ * lexical and nothing else changes. A channel that is registered but not ready
86
+ * yet - the model is still downloading, or the download failed and is waiting to
87
+ * be retried - answers `ready()` with `false`, an empty `search`, and keeps
88
+ * what `add` handed it until it is.
89
+ *
90
+ * The ref is the store's own: an event ref for the journal, a note id for the
91
+ * note store, which holds one vector per note and replaces it when the note is
92
+ * revised. What the channel keeps beside a ref - the fields, and the order the
93
+ * ref's number carries - is whatever that store's reads filter on.
94
+ */
95
+ export interface SemanticIndex {
96
+ /** Embed and store one item's text, replacing what is stored for it. */
97
+ add(ref: Ref, text: string, fields?: SemanticFields): Promise<void>;
98
+ /** Forget one item's text, as a redaction or a delete does. */
99
+ remove(ref: Ref): Promise<void>;
100
+ /** The refs nearest a query, best first, already filtered. Empty until ready. */
101
+ search(query: string, limit: number, filters: SemanticFilters): Promise<readonly Ref[]>;
102
+ /** Whether the channel can answer a query now. */
103
+ ready(): Promise<boolean>;
104
+ /** Every ref the channel holds, so a caller can index what it is missing. */
105
+ held(): Promise<ReadonlySet<Ref>>;
106
+ /** Release the underlying store. */
107
+ close(): Promise<void>;
108
+ }
55
109
  export interface Journal {
56
110
  /** Append one event. Assigns `ref` and `seq`; a redelivered `external` id is stored once. */
57
111
  append(input: EventInput): Promise<Event>;
@@ -63,7 +117,11 @@ export interface Journal {
63
117
  list(options?: ListOptions): Promise<EventPage>;
64
118
  /** The events around one, in the global order. */
65
119
  around(ref: EventRef, options?: AroundOptions): Promise<Event[]>;
66
- /** Lexical search: exact substring hits first, then word hits, each with its ref and window. */
120
+ /**
121
+ * Text search: exact substring hits first, then word hits and semantic hits
122
+ * fused by reciprocal rank, each with its ref and window
123
+ * (docs/journal.md#search).
124
+ */
67
125
  search(query: string, options?: SearchOptions): Promise<SearchPage>;
68
126
  /** Related items by weight, with the call that pages the rest. */
69
127
  links(ref: Ref, options?: LinksOptions): Promise<LinkPage>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4fu/arbor-interfaces",
3
- "version": "2026.1106.1",
3
+ "version": "2026.1107.0",
4
4
  "type": "module",
5
5
  "description": "Every replaceable interface in arbor, and the core's own shapes.",
6
6
  "exports": {
@@ -66,6 +66,6 @@
66
66
  "access": "public"
67
67
  },
68
68
  "dependencies": {
69
- "@4fu/arbor-errors": "2026.1106.1"
69
+ "@4fu/arbor-errors": "2026.1107.0"
70
70
  }
71
71
  }
package/plugin.d.ts CHANGED
@@ -17,7 +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
+ import type { ExternalTool, ExternalToolSource } from "./tools.ts";
21
21
  /** Connections to the outside world. */
22
22
  export interface PluginAdapters {
23
23
  channels?: readonly Channel[];
@@ -68,8 +68,13 @@ export interface Plugin {
68
68
  * them, and called in the turn through the same path a core tool takes
69
69
  * (docs/tools.md#external-tools). A name a core tool or a module already
70
70
  * declares is not registered; the host records the collision with its rule.
71
+ *
72
+ * The set may be a list or a source the host calls again on every refresh, so
73
+ * a plugin whose tools change while the host runs - the tools a harness
74
+ * around the host registered - answers with what stands now
75
+ * (docs/tools.md#external-tools-that-change).
71
76
  */
72
- tools?: readonly ExternalTool[];
77
+ tools?: readonly ExternalTool[] | ExternalToolSource;
73
78
  hooks?: Hooks;
74
79
  promptSection?: PromptSection;
75
80
  /** Extra criteria or decisions for its own inputs. */
package/refs.d.ts CHANGED
@@ -41,8 +41,11 @@ export type Target = string;
41
41
  export type EventKind = "input" | "tool_call" | "tool_result" | "send" | "effect_result" | "note_revision" | "window_start" | "window_end" | "delivery" | "environment_revision" | "service_state" | "redaction" | "schedule" | (string & {});
42
42
  /** Link kinds between refs. The host records them; the model never declares one. */
43
43
  export type LinkKind = "cite" | "evidence" | "range" | "origin" | "revision" | "handoff" | "continuation" | "notice" | "reply" | (string & {});
44
- /** How a search hit matched: an exact substring, or a word. */
45
- export type MatchKind = "substring" | "word";
44
+ /**
45
+ * How a search hit matched: an exact substring, a word, or meaning alone. A hit
46
+ * the semantic channel found and no lexical index did is `semantic`.
47
+ */
48
+ export type MatchKind = "substring" | "word" | "semantic";
46
49
  /** Page size default shared by every read that returns a page. */
47
50
  export declare const DEFAULT_PAGE_LIMIT = 20;
48
51
  /** Maximum page size any read returns. */
package/tools.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * model can otherwise do with a plugin is an MCP server and skills in `/env`.
10
10
  */
11
11
  import type { DirectToolSpec, FunctionSpec, ModuleSpec, ParameterSpec, ToolCall, ToolResult } from "./agent.ts";
12
+ import type { JSONSchema } from "./agent.ts";
12
13
  import type { EventRef, WindowRef } from "./refs.ts";
13
14
  import type { WorkspaceMount } from "./workspace.ts";
14
15
  /** What a call is made in. The service layer fills it; a group never reaches past it. */
@@ -47,7 +48,35 @@ export interface ExternalTool {
47
48
  readonly name: string;
48
49
  /** What it does and when to use it; side effects. 1 to 4 sentences. */
49
50
  readonly description: string;
50
- readonly parameters: Record<string, ParameterSpec>;
51
+ /**
52
+ * The parameters in arbor's own declaration shape. Optional for a tool that
53
+ * declares `schema` instead: an outside harness already has a JSON Schema for
54
+ * its tool, and restating it in this shape would only lose what the schema
55
+ * says.
56
+ */
57
+ readonly parameters?: Record<string, ParameterSpec>;
58
+ /**
59
+ * The parameters as the JSON Schema the outside harness declared them,
60
+ * carried verbatim (docs/tools.md#external-tools). What the model reads when
61
+ * the tool has one; `parameters` is the declaration for a tool that has none.
62
+ */
63
+ readonly schema?: JSONSchema;
51
64
  /** Run one call. The same path a core tool takes. */
52
65
  execute(request: ToolCall, context: CallContext): Promise<ToolResult>;
53
66
  }
67
+ /**
68
+ * Where a plugin's external tools come from: a list, or a call that answers
69
+ * with the current one. The host reads the source again on every refresh, so a
70
+ * plugin whose tools change at runtime - the tools a harness around the host
71
+ * registered - answers with what stands now
72
+ * (docs/tools.md#external-tools-that-change).
73
+ */
74
+ export type ExternalToolSource = () => readonly ExternalTool[] | Promise<readonly ExternalTool[]>;
75
+ /** What one refresh of the external tools changed about one tool. */
76
+ export interface ExternalToolChange {
77
+ change: "added" | "removed";
78
+ /** The tool's own name, as the model calls it. */
79
+ name: string;
80
+ /** Who declares it: the plugin's name, or the backend's. */
81
+ source: string;
82
+ }