@skanl/brambo-session 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,300 @@
1
+ import type { CliExecutorAdapterOptions } from '@skanl/brambo-adapter-cli';
2
+ import type { ExecutorAdapter, ResultEnvelope, SandboxPolicy, SandboxProvider, ToolExecutionContext, ToolExecutor, ToolInvocation, ToolResult, WorkspaceProvider } from '@skanl/brambo-contracts';
3
+ import { type ActionPolicy, type LogSink, type BramboKernel } from '@skanl/brambo-kernel';
4
+ import { type ExecutorConfigLayers, type ExecutorSelection } from './executors.ts';
5
+ /**
6
+ * The PREFIX every session invocation is recorded under. The registered id is
7
+ * `${SESSION_ACTION_ID}#${workspace.id}`, not this constant: a pipeline rejects a
8
+ * duplicate registration, and since Story M3.B the pipeline belongs to the
9
+ * KERNEL rather than to the session, so two sessions sharing one kernel really
10
+ * do register into the same set of ids. Scoping to the workspace is what keeps
11
+ * them distinguishable — and workspace ids are UUIDs on the default provider.
12
+ *
13
+ * Exported because a reader of the record stream needs the same string the
14
+ * pipeline wrote; match with `subject.startsWith(SESSION_ACTION_ID + '#')`.
15
+ */
16
+ export declare const SESSION_ACTION_ID = "session.executor-run";
17
+ /**
18
+ * What one executor run is ADMITTED at, before the vendor says what it spent.
19
+ *
20
+ * The upgrade path the previous note named is now built. Story M3.C gave the
21
+ * pipeline settlement — an action is admitted on this estimate and reconciled
22
+ * against the executor's own reported usage when the run resolves — so within a
23
+ * single session `maxTotalCost` and `maxInvocations` finally refuse on DIFFERENT
24
+ * runs: one run of claude-code settles at tens of thousands of tokens, so a cost
25
+ * cap fires while the invocation count is still 1, and an invocation cap fires
26
+ * while the settled cost is far under its own limit.
27
+ *
28
+ * ponytail: this stays a flat 1 on purpose, and it is the last piece of the old
29
+ * collapse still standing. Brambo may not invent a pre-run token figure — no
30
+ * estimating, no tokenizer, that is the whole point of settling instead — and
31
+ * raising it to a token-scale placeholder would silently redefine every cap
32
+ * already written against "1 = one run". A host that budgets in tokens builds
33
+ * its own kernel (`createSessionKernel`) and can pass its own estimate through
34
+ * `createExecutorPlugin({ cost })`; the settlement corrects it either way. What
35
+ * remains open: a run whose vendor reports nothing is charged this 1, so a
36
+ * session against such an executor is still capped by count rather than by
37
+ * spend (deferred-work.md).
38
+ */
39
+ export declare const SESSION_ACTION_COST = 1;
40
+ /** The request `executeTool` puts before the host's approval boundary. */
41
+ export interface ToolApprovalRequest {
42
+ readonly invocation: ToolInvocation;
43
+ readonly context: ToolExecutionContext;
44
+ }
45
+ /** The host decides whether a normalized tool invocation may proceed. */
46
+ export type ToolApproval = (request: ToolApprovalRequest) => boolean | Promise<boolean>;
47
+ /** A completed normalized tool invocation, for host-owned observation. */
48
+ export interface ToolExecutionEvent extends ToolApprovalRequest {
49
+ readonly result: ToolResult;
50
+ }
51
+ /** Options for one host-approved SDK tool invocation. */
52
+ export interface ExecuteToolOptions extends ToolCompositionOptions {
53
+ readonly invocation: ToolInvocation;
54
+ readonly context: ToolExecutionContext;
55
+ }
56
+ /**
57
+ * Executes one validated tool through the supplied ToolExecutor.
58
+ *
59
+ * This is deliberately separate from `runSession`: executor runs are vendor
60
+ * sessions, while tool calls are host-controlled SDK operations. The helper
61
+ * makes the approval boundary executable without teaching the kernel about
62
+ * tools, processes, filesystems, or sandbox backends.
63
+ */
64
+ export declare function executeTool(options: ExecuteToolOptions): Promise<ToolResult>;
65
+ /** A lifecycle event emitted when a sandbox-backed SDK flow selects a provider. */
66
+ export interface SandboxEvent {
67
+ readonly type: 'provider-selected' | 'session-created' | 'session-disposed';
68
+ readonly providerId: string;
69
+ readonly policy: SandboxPolicy;
70
+ }
71
+ /**
72
+ * Tool composition dependencies for the SDK execution boundary. `runSession`
73
+ * remains a vendor-run lifecycle; `executeTool` routes explicit host-approved
74
+ * invocations without making the vendor executor or kernel a tool registry.
75
+ */
76
+ export interface ToolCompositionOptions {
77
+ readonly sandboxProvider?: SandboxProvider;
78
+ readonly toolExecutor?: ToolExecutor;
79
+ readonly toolPolicy?: SandboxPolicy;
80
+ readonly approveTool?: ToolApproval;
81
+ readonly onToolExecution?: (event: ToolExecutionEvent) => void;
82
+ readonly onSandboxEvent?: (event: SandboxEvent) => void;
83
+ }
84
+ export interface SessionOptions extends ToolCompositionOptions {
85
+ /** Handed to the executor verbatim; rejected before anything is created if it is blank. */
86
+ readonly prompt: string;
87
+ /** Root the mounted workspace plugin builds `.brambo/workspaces` under. Defaults to `process.cwd()`. */
88
+ readonly cwd?: string;
89
+ /**
90
+ * Which shipped adapter runs the prompt, by catalogue id (`executors.ts`).
91
+ * Omitted, the selection comes from `configLayers` and then from brambo's
92
+ * built-in default LAYER — the default is a lookup like every other id, so no
93
+ * path here constructs a vendor adapter by name.
94
+ *
95
+ * Set as the `invocation` layer of the kernel's configuration, so it wins over
96
+ * every document and is reported as having done so.
97
+ */
98
+ readonly executorId?: string;
99
+ /**
100
+ * Brambo's own configuration documents, ALREADY READ (`readExecutorConfigLayers`).
101
+ *
102
+ * This is what seeds the kernel's layered configuration, so the mounted
103
+ * plugins and the executor selection read one composed document. It is DATA,
104
+ * never a path: a session primitive that read files under the running user's
105
+ * home would be unusable from a host that already knows what it wants, and it
106
+ * would make every `brambo run` test depend on whoever ran the suite. Omitted,
107
+ * only brambo's `defaults` layer and `executorId` apply.
108
+ */
109
+ readonly configLayers?: ExecutorConfigLayers;
110
+ /**
111
+ * Options handed to the SELECTED adapter: a child-process spawner, or a binary
112
+ * path that overrides the trait's command. Ignored when `createAdapter` is
113
+ * supplied, because then the caller built the adapter itself.
114
+ *
115
+ * This is the seam that makes `executorId` provable end to end — a fake
116
+ * spawner here exercises selection, catalogue lookup and vendor argv on the
117
+ * PRODUCTION path, where injecting `createAdapter` bypasses the very wiring
118
+ * under test. It is also what gives an embedding host a way to point brambo at
119
+ * a binary that is not on PATH.
120
+ */
121
+ readonly adapterOptions?: CliExecutorAdapterOptions;
122
+ /**
123
+ * Adapter seam; tests and embedding hosts inject their own. When supplied it
124
+ * WINS over `executorId` and `adapterOptions`: the caller handed brambo the
125
+ * executor, so brambo did not select one. The invocation still travels the
126
+ * kernel's waterfall — the seam decides WHICH executor runs, never WHETHER the
127
+ * pipeline sees it.
128
+ */
129
+ readonly createAdapter?: () => ExecutorAdapter;
130
+ /**
131
+ * Workspace provider seam. Omitted, the provider comes from the kernel's
132
+ * mounted `workspace` plugin, which is what `brambo run` uses.
133
+ *
134
+ * OWNERSHIP: the session disposes whatever this returns, on every path. Hand
135
+ * back a FRESH provider per session — returning a pooled or long-lived one
136
+ * leaves it disposed, and the next session against it fails with
137
+ * `BRAMBO_CONTRACT_PROVIDER_DISPOSED` (pinned by a test, because the obvious
138
+ * reason to inject a provider is to pool workspaces). A provider obtained from
139
+ * the kernel is NOT disposed here: it belongs to the plugin, and the kernel
140
+ * disposes it at `stop()`.
141
+ */
142
+ readonly createProvider?: () => WorkspaceProvider;
143
+ /**
144
+ * Signal-registration seam: register a handler for interrupt/termination and
145
+ * return its disposer. Deliberately has NO default — a library that installs
146
+ * `process.on('SIGINT')` steals the signal from whatever host embedded it, so
147
+ * the process owner supplies this. `@skanl/brambo-cli` passes its SIGINT/SIGTERM
148
+ * wiring here; an SDK caller with its own cancellation passes its own.
149
+ */
150
+ readonly onInterrupt?: (handler: () => void) => () => void;
151
+ /**
152
+ * Told which executor was selected and which layer decided it, BEFORE anything
153
+ * is constructed. `brambo run` prints that line on stderr; a host that offers a
154
+ * choice renders it however it likes.
155
+ *
156
+ * A throw here is contained: a reporter is an observer, and an observer must
157
+ * not be able to fail a run it was only watching.
158
+ */
159
+ readonly onSelection?: (selection: ExecutorSelection) => void;
160
+ /**
161
+ * Told about a configuration key brambo READ and could not use — an unknown key
162
+ * inside a mounted plugin's subtree, or a subtree of the wrong shape.
163
+ *
164
+ * These are reported and survived rather than fatal, and this is the seam that
165
+ * keeps "reported" from meaning "emitted where nobody looks". `brambo run`
166
+ * prints them on stderr. Measured before it existed: a single forward-looking
167
+ * key in `~/.brambo/config.json` failed every run on the machine with
168
+ * `BRAMBO_KERNEL_PLUGIN_START_FAILED`. Contained, like `onSelection`.
169
+ */
170
+ readonly onWarning?: (message: string) => void;
171
+ /**
172
+ * Where the interception waterfall's records go. Omitted, the session builds
173
+ * an in-memory sink it then drops — the waterfall still runs, its trail is
174
+ * simply unread.
175
+ *
176
+ * The WATERFALL's, not the kernel's whole stream: the kernel this session owns
177
+ * also records manifest validation, activation and disposal, and those stay in
178
+ * the kernel's own sink. A caller who wants the complete stream builds the
179
+ * kernel itself (`createKernel({ log })`) and passes it as `kernel`.
180
+ *
181
+ * ponytail: the caller owns the sink they pass, draining included. A sink whose
182
+ * write is async still has records in flight when this resolves; `await
183
+ * sink.drain()` before reading `sink.records`.
184
+ */
185
+ readonly log?: LogSink;
186
+ /**
187
+ * Declarative caps for this session's kernel pipeline (AD-10). Omitted, nothing
188
+ * is capped, which is what keeps `brambo run` behaviour-neutral. A violation is
189
+ * refused BEFORE the executor runs and surfaces as a coded `BramboKernelError`.
190
+ *
191
+ * Per KERNEL, so a host that shares one kernel across sessions caps all of
192
+ * them together — which is the whole reason `kernel` exists as an option.
193
+ */
194
+ readonly actionPolicy?: ActionPolicy;
195
+ /**
196
+ * A kernel this session should COMPOSE THROUGH instead of building its own.
197
+ *
198
+ * OWNERSHIP: the caller owns it — the caller mounted its plugins, seeded its
199
+ * configuration and must `stop()` it; this session never does, so several
200
+ * sessions can share one pipeline and one budget. That sharing is the point:
201
+ * a cap only means something while every invocation it is supposed to bound
202
+ * goes through the same pipeline.
203
+ *
204
+ * Every option that would configure a kernel, choose its executor, place its
205
+ * workspaces or report on any of that is REFUSED alongside it rather than
206
+ * ignored, because a budget or an executor selection that silently did nothing
207
+ * is worse than one that was rejected. `createProvider` is refused too: it
208
+ * exists for pooling, pooling gives a stable workspace id, a stable workspace
209
+ * id gives a stable ACTION id, and a kernel-owned pipeline never retires one —
210
+ * so the second run failed `BRAMBO_KERNEL_ACTION_INVALID` on a shared kernel.
211
+ * A supplied kernel already carries a workspace provider; that is the point.
212
+ */
213
+ readonly kernel?: BramboKernel;
214
+ }
215
+ export interface SessionKernelOptions extends ToolCompositionOptions {
216
+ /**
217
+ * Root the workspace plugin builds `.brambo/workspaces` under. NAMED or not is
218
+ * load-bearing: named, it is this invocation's answer and wins over every
219
+ * document; omitted, `process.cwd()` supplies a DEFAULTS layer that a
220
+ * `workspace.rootDir` in the user's document overrides. Anything else would
221
+ * make the layered configuration decorative for the one object-namespaced
222
+ * plugin brambo mounts — measured: a valid configured `rootDir` was validated
223
+ * and then always discarded.
224
+ */
225
+ readonly cwd?: string;
226
+ /** Explicit executor selection for this invocation; the `invocation` layer. */
227
+ readonly executorId?: string;
228
+ /** Brambo's own documents, already read (`readExecutorConfigLayers`). */
229
+ readonly configLayers?: ExecutorConfigLayers;
230
+ /** Options handed to the selected adapter. */
231
+ readonly adapterOptions?: CliExecutorAdapterOptions;
232
+ /** Adapter seam; wins over the selection, and still runs through the waterfall. */
233
+ readonly createAdapter?: () => ExecutorAdapter;
234
+ /** Where the KERNEL's whole record stream goes — lifecycle transitions included. */
235
+ readonly log?: LogSink;
236
+ /** Declarative caps for this kernel's pipeline, shared by every session on it. */
237
+ readonly actionPolicy?: ActionPolicy;
238
+ /** See `SessionOptions.onSelection`; called before any plugin is registered. */
239
+ readonly onSelection?: (selection: ExecutorSelection) => void;
240
+ /** See `SessionOptions.onWarning`. */
241
+ readonly onWarning?: (message: string) => void;
242
+ }
243
+ /**
244
+ * A kernel with brambo's two plugins mounted, its configuration seeded from
245
+ * brambo's own documents, and its plugins started.
246
+ *
247
+ * This is the ONE composition. `runSession` calls it when no kernel is passed,
248
+ * and a host that wants several sessions to share one pipeline, one budget and
249
+ * one record stream calls it directly and passes the result as
250
+ * `SessionOptions.kernel`.
251
+ *
252
+ * It exists as a single named surface on purpose. `@skanl/brambo-session` briefly
253
+ * re-exported `createKernel` and both plugin FACTORIES so a host could assemble
254
+ * this itself, and that was a hole rather than a convenience: a `PluginFactory`
255
+ * invoked with an `ActivationContext` of the caller's own construction hands
256
+ * back a real vendor adapter wired to the caller's own pipeline, so the bypass
257
+ * surface of a session-only consumer went from nothing to one. Handing back a
258
+ * started kernel gives a host the capability without the factory.
259
+ *
260
+ * Throws before anything is constructed for a selection brambo has no adapter
261
+ * for, and stops the kernel again if any plugin fails to activate.
262
+ */
263
+ export declare function createSessionKernel(options?: SessionKernelOptions): BramboKernel;
264
+ /**
265
+ * One brambo session: compose through a kernel, create a workspace, run the
266
+ * prompt under a cancellation signal through the kernel's interception
267
+ * waterfall, then release and dispose whatever happened.
268
+ *
269
+ * This is the composition `brambo run` performs, and it lives here rather than in
270
+ * `@skanl/brambo-cli` so a third party gets it by importing packages (PRD §2, ROADMAP-01
271
+ * Correction A). The CLI adds argv parsing, JSON formatting and exit codes on top
272
+ * and nothing else.
273
+ *
274
+ * Since Story M3.B the adapter and the provider are MOUNTED, not constructed:
275
+ * `createExecutorPlugin` and `createWorkspacePlugin` are registered on a kernel,
276
+ * their configuration comes from one composed document, and their services are
277
+ * consumed by name. What that buys beyond hygiene: the executor invocation is an
278
+ * action on the KERNEL's pipeline, so a host that shares one kernel across
279
+ * sessions shares one budget — and the observability log exists before any
280
+ * plugin loads, which `loadPlugins`' required sink parameter makes a type error
281
+ * to violate rather than a comment.
282
+ *
283
+ * The honest scope of the no-bypass claim: neither the kernel nor the `executor`
284
+ * service exports a path around the waterfall. Any package may still import
285
+ * `@skanl/brambo-adapter-cli` and drive a vendor adapter itself, and a caller that keeps
286
+ * a reference to the adapter it passed to `createAdapter` can invoke it after a
287
+ * refusal. Both are recorded as open in deferred-work.md.
288
+ *
289
+ * SIDE EFFECT: the mounted provider creates a directory per session under
290
+ * `<cwd>/.brambo/workspaces/<uuid>` and NOTHING removes it. `release()` ends a
291
+ * lease and `dispose()` deliberately leaves the tree in place so work survives —
292
+ * retention is the caller's problem (deferred-work.md).
293
+ *
294
+ * Failure surfaces as a throw (a coded `BramboError` from the workspace port or
295
+ * from a plugin that never activated, a coded `BramboKernelError` from a refusal,
296
+ * or whatever the adapter threw), because an envelope is what an executor RAN
297
+ * produces — returning a synthetic one for a workspace that never existed would
298
+ * make the two indistinguishable.
299
+ */
300
+ export declare function runSession(options: SessionOptions): Promise<ResultEnvelope>;