@fugood/buttress-server 2.26.0-beta.2 → 2.26.0-beta.21

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.
Files changed (70) hide show
  1. package/README.md +415 -10
  2. package/config/function-samples/README.md +1 -0
  3. package/config/function-samples/classify-ticket.ts +73 -0
  4. package/config/function-samples/run-agent.ts +39 -0
  5. package/config/sample.toml +67 -0
  6. package/lib/agent/cli.d.ts +19 -0
  7. package/lib/agent/client.d.ts +66 -0
  8. package/lib/agent/codemode-worker.d.ts +1 -0
  9. package/lib/agent/codemode.d.ts +7 -0
  10. package/lib/agent/config.d.ts +15 -0
  11. package/lib/agent/context.d.ts +11 -0
  12. package/lib/agent/decisions.check.d.ts +1 -0
  13. package/lib/agent/decisions.d.ts +113 -0
  14. package/lib/agent/device-auth.d.ts +3 -0
  15. package/lib/agent/device-tools.d.ts +5 -0
  16. package/lib/agent/loopback.d.ts +21 -0
  17. package/lib/agent/mcp.d.ts +22 -0
  18. package/lib/agent/models.d.ts +22 -0
  19. package/lib/agent/service.d.ts +16 -0
  20. package/lib/agent/sessions.d.ts +30 -0
  21. package/lib/agent/tools.d.ts +35 -0
  22. package/lib/agent/tui.d.ts +17 -0
  23. package/lib/agent/types.d.ts +155 -0
  24. package/lib/cli-KbjOekMI.mjs +22 -0
  25. package/lib/client-qqI-HCOj.mjs +8 -0
  26. package/lib/codemode-Cva7OeQX.mjs +112 -0
  27. package/lib/codemode-worker.mjs +3 -0
  28. package/lib/command-nXCdWBlc.mjs +38 -0
  29. package/lib/config-DiYweJ2m.mjs +2 -0
  30. package/lib/functions/executor.d.ts +7 -1
  31. package/lib/functions/index.d.ts +3 -1
  32. package/lib/functions/status.d.ts +1 -1
  33. package/lib/functions/templates.d.ts +1 -1
  34. package/lib/functions/types.d.ts +15 -2
  35. package/lib/index.d.ts +12 -5
  36. package/lib/index.mjs +146 -93
  37. package/lib/mcp-DzknURyz.mjs +5 -0
  38. package/lib/mlx-bridge.py +707 -0
  39. package/lib/net-OvedjqrB.mjs +46 -0
  40. package/lib/protocol-WkPByalT.mjs +468 -0
  41. package/lib/quickjs.wasm +0 -0
  42. package/lib/rolldown-runtime-dTnj95Mm.mjs +2 -0
  43. package/lib/routes/agents.d.ts +66 -0
  44. package/lib/routes/decision-shared.d.ts +28 -0
  45. package/lib/routes/generator-cache.d.ts +3 -1
  46. package/lib/routes/index.d.ts +2 -0
  47. package/lib/routes/llm-shared.d.ts +8 -0
  48. package/lib/routes/openai-compat.d.ts +13 -2
  49. package/lib/routes/openai-decisions.check.d.ts +1 -0
  50. package/lib/routes/openai-decisions.d.ts +97 -0
  51. package/lib/routes/status.d.ts +2 -1
  52. package/lib/routes/typesafe-compat.check.d.ts +1 -0
  53. package/lib/routes/typesafe-compat.d.ts +30 -0
  54. package/lib/services/ggml-decision.d.ts +80 -0
  55. package/lib/services/ggml-llm.d.ts +29 -1
  56. package/lib/services/index.d.ts +3 -0
  57. package/lib/tui-ClNupsCH.mjs +2 -0
  58. package/lib/types.d.ts +48 -2
  59. package/lib/ui/command.d.ts +2 -0
  60. package/lib/ui/host.d.ts +1 -0
  61. package/lib/ui/window-module.d.ts +6 -0
  62. package/lib/utils/config.d.ts +3 -3
  63. package/lib/utils/cors.d.ts +1 -0
  64. package/lib/utils/decision-sharing.d.ts +37 -0
  65. package/lib/utils/downloadStatus.d.ts +57 -0
  66. package/lib/utils/hostFacts.d.ts +13 -0
  67. package/lib/utils/workspaceState.d.ts +8 -0
  68. package/lib/wrapper-3PX6qE3t.mjs +7 -0
  69. package/package.json +17 -9
  70. package/public/status.html +219 -1489
@@ -0,0 +1,73 @@
1
+ // Triage a support ticket with this server's typed decision model: which team
2
+ // takes it, how urgent it is, and two yes/no flags — calibrated probabilities
3
+ // from one forward pass per answer, no text generation and no output parsing.
4
+ //
5
+ // Needs a ggml-decision generator in the server config, e.g.:
6
+ //
7
+ // [[generators]]
8
+ // type = "ggml-decision"
9
+ // [generators.model]
10
+ // repo_id = "ggml-org/Julia-1-GGUF"
11
+ // filename = "Julia-1-Q8_0.gguf"
12
+ //
13
+ // curl -X POST http://<host>:<port>/functions/classify-ticket \
14
+ // -H 'Content-Type: application/json' \
15
+ // -d '{"message": "I was charged twice and nobody replies. I want my money back today."}'
16
+ // # → { "result": { "route": "billing", "urgency": 1.53, "urgencyLabel": "Today", "angry": true, … } }
17
+
18
+ export const meta: ButtressFunctionMeta = {
19
+ description:
20
+ 'Triage a support ticket: route it to a team, rate its urgency, and flag anger or a refund request',
21
+ parameters: {
22
+ type: 'object',
23
+ properties: {
24
+ message: { type: 'string', description: 'The customer message' },
25
+ model: { type: 'string', description: 'Optional decision model override' },
26
+ },
27
+ required: ['message'],
28
+ },
29
+ timeout: '2m',
30
+ }
31
+
32
+ type Input = { message: string; model?: string }
33
+
34
+ const URGENCY_LEVELS = ['Can wait', 'This week', 'Today', 'Right now']
35
+
36
+ export default async function ({ message, model }: Input, context: ButtressFunctionContext) {
37
+ const { answers, usage } = await context.buttress.decide({
38
+ model,
39
+ state: message,
40
+ questions: {
41
+ route: {
42
+ type: 'choice',
43
+ instructions: 'Which team should handle this?',
44
+ criteria: {
45
+ billing: 'Payments, charges, refunds',
46
+ shipping: 'Delivery and tracking',
47
+ technical: 'App or website problems',
48
+ account: 'Login and profile',
49
+ },
50
+ },
51
+ urgency: { type: 'score', instructions: 'How urgent is this?', criteria: URGENCY_LEVELS },
52
+ angry: { type: 'noul', instructions: 'The customer is angry' },
53
+ wants_refund: { type: 'noul', instructions: 'The customer asks for a refund' },
54
+ },
55
+ })
56
+
57
+ // `answers.route.choice` is typed as 'billing' | 'shipping' | 'technical' | 'account'
58
+ return {
59
+ route: answers.route.choice,
60
+ routeConfidence: answers.route.confidence,
61
+ // Expected level index (0 = "Can wait"), can fall between two levels
62
+ urgency: answers.urgency.score,
63
+ urgencyLabel: URGENCY_LEVELS[Math.round(answers.urgency.score)],
64
+ angry: answers.angry.noul >= 0.5,
65
+ wantsRefund: answers.wants_refund.noul >= 0.5,
66
+ probabilities: {
67
+ route: answers.route.probabilities,
68
+ angry: answers.angry.noul,
69
+ wantsRefund: answers.wants_refund.noul,
70
+ },
71
+ inputTokens: usage.input_tokens,
72
+ }
73
+ }
@@ -0,0 +1,39 @@
1
+ // Local functions can drive a configured agent (see [[agents]] in the server
2
+ // config): context.agents.run executes a full tool-using LLM loop server-side
3
+ // and returns the final answer plus the session id for follow-ups.
4
+ //
5
+ // The run shares this call's lifetime — the function's deadline (meta.timeout)
6
+ // and client disconnects abort it, including in-flight tool calls. Long agent
7
+ // work belongs in a daemon (no deadline) instead.
8
+
9
+ export const meta = {
10
+ description: 'Ask a configured agent to investigate something and return its conclusion.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ agent: { type: 'string', description: 'Configured agent name' },
15
+ question: { type: 'string' },
16
+ sessionId: { type: 'string', description: 'Continue an earlier session (optional)' },
17
+ },
18
+ required: ['agent', 'question'],
19
+ },
20
+ timeout: '10m',
21
+ }
22
+
23
+ export default async (
24
+ { agent, question, sessionId }: { agent: string; question: string; sessionId?: string },
25
+ context: any,
26
+ ) => {
27
+ const result = await context.agents.run(agent, {
28
+ prompt: question,
29
+ sessionId,
30
+ // Progress mirrors onto this call's SSE stream automatically; onEvent is
31
+ // available for custom handling of the raw pi events.
32
+ })
33
+ return {
34
+ conclusion: result.content,
35
+ sessionId: result.sessionId,
36
+ turns: result.usage.totalTurns,
37
+ stopReason: result.stopReason,
38
+ }
39
+ }
@@ -29,6 +29,13 @@ enabled = true
29
29
  # cors_allowed_origins = ["http://localhost:3000", "https://example.com"]
30
30
  # cors_allowed_origins = "*"
31
31
 
32
+ # TypeSafe-compatible System One API (`/typesafe-compat/v1/systemone`) for the
33
+ # TypeSafe SDKs: TYPESAFE_BASE_URL=http://<host>:<port>/typesafe-compat.
34
+ # Serves the decision generators below.
35
+ # [typesafe_compat]
36
+ # enabled = true
37
+ # cors_allowed_origins = ["http://localhost:3000"]
38
+
32
39
  # Local functions (EXPERIMENTAL): .ts/.js files exposed as MCP tools and HTTP
33
40
  # endpoints. Interfaces may change between releases.
34
41
  # See the "Local Functions" section of README.md; ready-to-copy examples live
@@ -45,6 +52,31 @@ enabled = true
45
52
  # api_base = "https://example.internal"
46
53
  # api_keys = ["a-long-random-string"] # e.g. consumed by function-samples/_auth.ts
47
54
 
55
+ # Agents (EXPERIMENTAL): pi-based LLM loops running in this server, with local
56
+ # functions (and MCP servers) as tools and config-scoped sessions on disk.
57
+ # Local functions call them via context.agents.run(...); an interactive CLI
58
+ # (`bricks-buttress agent <name> -c <config>`) drives the same agents.
59
+ # See the "Agents" section of README.md.
60
+ # [[agents]]
61
+ # name = "ops-assistant"
62
+ # model = "buttress/ggml-org/gpt-oss-20b-GGUF" # provider/model-id, split on the FIRST slash
63
+ # system_prompt = "You are an ops automation agent. Use the provided tools."
64
+ # tools = ["host-info"] # local function names (explicit; no wildcard)
65
+ # local_devices = false # opt in: devices_scan + devtools (inspection AND control)
66
+ # max_turns = 30
67
+ # decision_model = "buttress/<ggml-decision id>" # adds the decision_evaluate tool
68
+ # decision_assistance = false # opt in: skip tool definitions on runs that need no tool
69
+ # [agents.mcp_servers.example]
70
+ # url = "https://example.com/mcp/"
71
+ # optional = true
72
+
73
+ # [agents_options]
74
+ # sessions_dir = "./.buttress-agent/sessions"
75
+ # session_max_age = "30d" # retention; 0 disables
76
+ # session_max_count = 500 # per agent; 0 disables
77
+ # max_depth = 2 # function -> agent -> function chain cap
78
+ # allow_unauthenticated = false # serve /agents on an UNBOUND server
79
+
48
80
  [runtime]
49
81
  cache_dir = "./.buttress-cache"
50
82
  # huggingface_token = "hf_xx"
@@ -55,6 +87,12 @@ cache_type_k = "q8_0"
55
87
  cache_type_v = "q8_0"
56
88
  n_threads = 6
57
89
 
90
+ # Optional ggml-llm multi-GPU defaults (also supported under [generators.model]).
91
+ # split_mode = "layer" # none / layer (default) / row / tensor (experimental)
92
+ # tensor_split = [3, 1] # per-GPU proportions; also accepts "3,1"
93
+ # Tensor mode requires a compatible GPU backend and model architecture.
94
+ # TODO: Expose these Buttress-only options in the LLM (GGML) generator.
95
+
58
96
  # Delay before releasing context when refCount reaches 0 (default: 10000ms)
59
97
  # Set to 0 for immediate release
60
98
  context_release_delay_ms = 30000
@@ -100,6 +138,7 @@ n_ctx = 12800 # Max: 131072
100
138
 
101
139
  [[generators]]
102
140
  type = "ggml-llm"
141
+ # enabled = false # keep this entry in the file but skip it at startup (default: true)
103
142
  [generators.backend]
104
143
  variant_preference = ["cuda", "vulkan", "snapdragon", "default"]
105
144
  gpu_memory_fraction = 0.95
@@ -271,3 +310,31 @@ use_gpu = true
271
310
  [generators.runtime]
272
311
  n_ctx = 8192
273
312
  vocoder_batch_size = 4096
313
+
314
+ # Typed decision generators
315
+ #
316
+ # A typed decision model (System One) answers `choice` / `score` / `noul`
317
+ # questions about a state with calibrated probabilities — one forward pass per
318
+ # answer, no generated tokens. Clients reach it through the Typed Decision
319
+ # (GGML) generator, and local functions through `context.buttress.decide`.
320
+ # Uncomment to enable:
321
+ #
322
+ # [[generators]]
323
+ # type = "ggml-decision"
324
+ # [generators.model]
325
+ # repo_id = "ggml-org/Julia-1-GGUF"
326
+ # filename = "Julia-1-Q8_0.gguf"
327
+ # download = true
328
+ # # Models whose prompt takes images also need a projector:
329
+ # # mmproj_repo_id = "..."
330
+ # # mmproj_filename = "mmproj-....gguf"
331
+ # # system_one models with a classification head (rank_head readout):
332
+ # # rank_pooling = true
333
+ # [generators.runtime]
334
+ # n_ctx = 4096
335
+ #
336
+ # A model that also generates (Jevling, for one) can be configured as both a
337
+ # ggml-llm and a ggml-decision. To load it once, add to the decision entry
338
+ # [generators.backend]
339
+ # share_llm_context = true
340
+ # (decisions then compete with completions for the LLM's slots; see README).
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `bricks-buttress agent` — chat client for configured agents.
3
+ *
4
+ * A thin remote client of the /agents endpoints on a RUNNING server: the agent
5
+ * loop, tools, and sessions all live server-side. Reads the config file only
6
+ * to find the server port and the local runtime token (written 0600 by the
7
+ * server at startup), so the same-host flow needs zero extra setup.
8
+ *
9
+ * On a real terminal the pi-tui chat UI runs (see tui.ts); pipes/scripts (or
10
+ * --plain) get a line-based streaming renderer with identical semantics.
11
+ *
12
+ * bricks-buttress agent list configured agents
13
+ * bricks-buttress agent <name> chat (new session)
14
+ * bricks-buttress agent <name> --session <id> continue a session
15
+ * bricks-buttress agent <name> --fork <id> fork then chat
16
+ * bricks-buttress agent <name> --sessions list sessions
17
+ * Common: -c/--config <path|toml>, --url <server>, --token <token>, --plain
18
+ */
19
+ export declare const runAgentCommand: (argv: string[]) => Promise<void>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shared client plumbing for the `bricks-buttress agent` front-ends (line mode
3
+ * and the pi-tui chat): config/token discovery, authenticated requests, SSE
4
+ * parsing, and the streaming run call. UI-free on purpose.
5
+ */
6
+ export type Connection = {
7
+ baseUrl: string;
8
+ token: string | null;
9
+ };
10
+ /** Same config-argument semantics as the server CLI: a path or inline TOML. */
11
+ export declare const loadClientConfig: (configArg: string | null) => {
12
+ config: import("../types").Config;
13
+ configDir: string;
14
+ agentsConfig: import("./types").AgentsConfig | null;
15
+ };
16
+ export declare const readRuntimeToken: (file: string) => string | null;
17
+ export declare const request: (connection: Connection, route: string, init?: RequestInit) => Promise<Response>;
18
+ /** Minimal text/event-stream reader: yields { event, data } frames. */
19
+ export declare function readSse(body: ReadableStream<Uint8Array>): AsyncGenerator<{
20
+ event: string;
21
+ data: string;
22
+ }, void, unknown>;
23
+ export type RunFrame = {
24
+ event: string;
25
+ payload: any;
26
+ };
27
+ export type RunOutcome = {
28
+ kind: 'result';
29
+ result: any;
30
+ } | {
31
+ kind: 'error';
32
+ message: string;
33
+ sessionId: string | null;
34
+ } | {
35
+ kind: 'aborted';
36
+ sessionId: string | null;
37
+ } | {
38
+ kind: 'disconnected';
39
+ sessionId: string | null;
40
+ };
41
+ export type StreamRunBody = {
42
+ prompt: string;
43
+ sessionId?: string;
44
+ fork?: boolean;
45
+ };
46
+ /**
47
+ * Whether a `--fork` chat has actually forked yet, i.e. whether the flag has
48
+ * been consumed and later prompts should continue rather than fork again.
49
+ *
50
+ * The server only forks once it accepts the run, and announces the new id
51
+ * (`session` event, or `result.sessionId` on an older server). A run that
52
+ * failed before that — a rejected token, an unknown agent, a dropped
53
+ * connection, an immediate Ctrl+C — leaves the client on the SOURCE session, so
54
+ * the flag has to survive: clearing it would make the user's retry append to
55
+ * the transcript `--fork` exists to keep untouched.
56
+ */
57
+ export declare const forkTookEffect: (startedFrom: string | null, sessionId: string | null) => boolean;
58
+ /**
59
+ * Run a prompt over the streaming endpoint, delivering every parsed frame to
60
+ * `onFrame` and returning the terminal outcome. Frame handler errors are the
61
+ * caller's problem — they propagate.
62
+ */
63
+ export declare const streamRun: (connection: Connection, name: string, body: StreamRunBody, { signal, onFrame }: {
64
+ signal?: AbortSignal;
65
+ onFrame: (frame: RunFrame) => void;
66
+ }) => Promise<RunOutcome>;
@@ -0,0 +1 @@
1
+ import '@earendil-works/pi-codemode/worker';
@@ -0,0 +1,7 @@
1
+ import type { MutableModels } from '@earendil-works/pi-ai';
2
+ export declare function createAgentCodemodeTool({ tools, declaredNames, getState, models, }: {
3
+ tools: any[];
4
+ declaredNames: Set<string>;
5
+ getState: () => any;
6
+ models: MutableModels;
7
+ }): Promise<any>;
@@ -0,0 +1,15 @@
1
+ import type { Config } from '../types';
2
+ import type { AgentsConfig } from './types';
3
+ export declare class AgentsConfigError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export type ResolveAgentsOptions = {
7
+ /** Directory relative paths resolve against — the `--config` file's directory. */
8
+ configDir?: string;
9
+ };
10
+ /**
11
+ * Resolve `[[agents]]` + `[agents_options]` into the runtime shape. Returns
12
+ * null when no agents are configured. Invalid definitions throw — a wrong
13
+ * agent config should stop startup, not silently drop an agent.
14
+ */
15
+ export declare const resolveAgentsConfig: (config: Config, { configDir }?: ResolveAgentsOptions) => AgentsConfig | null;
@@ -0,0 +1,11 @@
1
+ import type { AgentsFunctionApi, AgentsService } from './types';
2
+ /**
3
+ * The `context.agents` surface local functions receive. Binds the caller's
4
+ * abort signal (function deadline / client disconnect cascades into the run),
5
+ * its emit stream, and its agent-invocation depth.
6
+ */
7
+ export declare const buildAgentsFunctionApi: (service: AgentsService | null | undefined, { signal, emit, depth, }: {
8
+ signal: AbortSignal;
9
+ emit?: (event: string, data?: unknown) => void;
10
+ depth: number;
11
+ }) => AgentsFunctionApi;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,113 @@
1
+ /**
2
+ * System One decisions for Buttress agents.
3
+ *
4
+ * - Buttress decision generators as pi-ai classifier models (`buttress/<id>`),
5
+ * answered in-process. Codemode `models.classify()` and the pieces below go
6
+ * through pi-ai, so a hosted classifier (`typesafe/jev-latest`) is
7
+ * interchangeable with a local one.
8
+ * - `decision_evaluate`: a tool the model calls itself with typed questions.
9
+ * - Tool prefilter (`decision_assistance`): before a run, three yes/no questions
10
+ * decide whether its request needs tools at all; a conversational request
11
+ * is sent without tool definitions, which shortens the prompt.
12
+ *
13
+ * A decision is evidence, never permission: it can only withhold tools for one
14
+ * run, never grant one, and any failure keeps the full tool set.
15
+ */
16
+ import type { ClassifierModel, ClassifierResult, MutableModels, ProviderClassifier } from '@earendil-works/pi-ai';
17
+ import type { AgentMessage } from '@earendil-works/pi-agent-core';
18
+ import type { Config, DecisionRequest, DecisionResult } from '../types';
19
+ import type { AgentDefinition } from './types';
20
+ export declare const BUTTRESS_DECISION_API = "buttress-decision";
21
+ export type DecideFn = (request: DecisionRequest & {
22
+ model?: string;
23
+ }) => Promise<DecisionResult>;
24
+ /** One pi-ai classifier model per configured decision generator. */
25
+ export declare const buttressClassifierModels: (config: Config, provider: string, baseUrl: string) => ClassifierModel<string>[];
26
+ /**
27
+ * Answers classifier requests with this server's decision generators. Never
28
+ * rejects. A decision cannot be stopped once started, so a caller that gives
29
+ * up (timeout, abort) leaves it running; until it settles, new requests for
30
+ * that model fail fast instead of piling up behind it (a cold model load
31
+ * would otherwise start one more per agent run).
32
+ */
33
+ export declare const createButtressClassifier: (decide: DecideFn) => ProviderClassifier;
34
+ /** The agent's decision model, or null when it has none. Throws on an unknown one. */
35
+ export declare const resolveDecisionModel: (models: MutableModels, agent: AgentDefinition) => ClassifierModel<any> | null;
36
+ export declare const DECISION_TOOL_NAME = "decision_evaluate";
37
+ /** Typed questions about a state the model supplies. Results never authorize actions. */
38
+ export declare const createDecisionEvaluateTool: (models: MutableModels, model: ClassifierModel<any>) => {
39
+ name: string;
40
+ label: string;
41
+ description: string;
42
+ parameters: {
43
+ type: string;
44
+ properties: {
45
+ state: {
46
+ type: string;
47
+ description: string;
48
+ };
49
+ questions: {
50
+ type: string;
51
+ description: string;
52
+ additionalProperties: {
53
+ type: string;
54
+ properties: {
55
+ type: {
56
+ type: string;
57
+ enum: string[];
58
+ };
59
+ instructions: {
60
+ type: string;
61
+ };
62
+ criteria: {};
63
+ };
64
+ required: string[];
65
+ };
66
+ };
67
+ };
68
+ required: string[];
69
+ };
70
+ execute: (_toolCallId: string, args: any, signal?: AbortSignal) => Promise<{
71
+ content: {
72
+ type: string;
73
+ text: string;
74
+ }[];
75
+ details: {};
76
+ }>;
77
+ };
78
+ /** Every probability must be below this to send a run without tools. */
79
+ export declare const PREFILTER_THRESHOLD = 0.35;
80
+ /**
81
+ * The prefilter's evidence: the request, the nearest user/assistant exchanges
82
+ * that fit, and only counts for what is left out. Tool arguments and results,
83
+ * reasoning and system messages are never sent.
84
+ */
85
+ export declare const buildPrefilterState: (prompt: string, history: readonly AgentMessage[], toolNames: string[]) => {
86
+ latestRequest: string;
87
+ recentConversation: {
88
+ role: string;
89
+ text: string;
90
+ }[];
91
+ availableTools: string[];
92
+ omittedContext: {
93
+ olderConversation: number;
94
+ toolPayloads: number;
95
+ };
96
+ };
97
+ export type PrefilterOutcome = {
98
+ prune: boolean;
99
+ reason: 'conversational' | 'action-or-uncertain' | 'unavailable' | 'skipped';
100
+ detail?: string;
101
+ };
102
+ /** Prune only on clear "no" answers to every question; anything else keeps every tool. */
103
+ export declare const interpretPrefilter: (result: ClassifierResult) => PrefilterOutcome;
104
+ /** Decide whether this run's request can go out without tool definitions. */
105
+ export declare const runToolPrefilter: ({ models, model, prompt, history, toolNames, timeoutMs, signal, }: {
106
+ models: MutableModels;
107
+ model: ClassifierModel<any>;
108
+ prompt: string;
109
+ history: readonly AgentMessage[];
110
+ toolNames: string[];
111
+ timeoutMs: number;
112
+ signal?: AbortSignal;
113
+ }) => Promise<PrefilterOutcome>;
@@ -0,0 +1,3 @@
1
+ import type { WorkspaceState } from '../utils/workspaceState';
2
+ /** Select only the bound workspace's credential; never reuse an inbound Buttress (k=ba) JWT. */
3
+ export declare const resolveDeviceAccessToken: (address: string, port: number, state?: WorkspaceState, signal?: AbortSignal) => Promise<string | undefined>;
@@ -0,0 +1,5 @@
1
+ import type { AgentFunctionTool } from './tools';
2
+ import type { AgentDefinition } from './types';
3
+ import type { WorkspaceState } from '../utils/workspaceState';
4
+ /** Two tools regardless of device count; no remote tool-list expansion or persistent sockets. */
5
+ export declare const buildLocalDeviceTools: (agent: Pick<AgentDefinition, 'localDevices'>, configDir: string, workspaceState?: WorkspaceState) => AgentFunctionTool[];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * In-process loopback for agent → buttress model traffic, plus the internal
3
+ * bearer token that authorizes it (and same-host CLIs) without a cloud-minted
4
+ * workspace JWT — a bound server only holds the issuer *public* key and
5
+ * cannot mint its own JWTs.
6
+ */
7
+ export type ElysiaHandle = (request: Request) => Response | Promise<Response>;
8
+ export declare const generateInternalToken: () => string;
9
+ export declare const makeInternalTokenVerifier: (token: string) => (candidate: string | null | undefined) => boolean;
10
+ /**
11
+ * Write the internal token where a same-host `bricks-buttress agent` CLI can
12
+ * read it (0600, next to the sessions dir — config-scoped like everything
13
+ * else about agents). Rewritten on every startup: the token is ephemeral.
14
+ */
15
+ export declare const writeRuntimeTokenFile: (file: string, token: string) => void;
16
+ /**
17
+ * Fetch-shaped dispatcher into the local Elysia app. The request never touches
18
+ * a socket; the internal token replaces whatever Authorization the client
19
+ * library synthesized so the shared auth guard accepts it on bound servers.
20
+ */
21
+ export declare const createLoopbackFetch: (getHandle: () => ElysiaHandle | null, token: string) => typeof fetch;
@@ -0,0 +1,22 @@
1
+ import type { AgentMcpServerConfig } from './types';
2
+ export type McpAgentTool = {
3
+ name: string;
4
+ label: string;
5
+ description: string;
6
+ parameters: Record<string, any>;
7
+ execute: (toolCallId: string, args: any, signal?: AbortSignal, onUpdate?: any) => Promise<any>;
8
+ _mcpServer: string;
9
+ _mcpToolName: string;
10
+ _mcpExposure: string;
11
+ };
12
+ export type McpManager = {
13
+ /**
14
+ * Tools for every configured server of an agent. Fail-closed: a server that
15
+ * won't connect rejects the whole call unless it is marked `optional`.
16
+ */
17
+ toolsFor: (agentName: string, servers: Record<string, AgentMcpServerConfig>) => Promise<McpAgentTool[]>;
18
+ dispose: () => Promise<void>;
19
+ };
20
+ export declare const createMcpManager: ({ configDir }: {
21
+ configDir: string;
22
+ }) => McpManager;
@@ -0,0 +1,22 @@
1
+ import { type Model, type MutableModels } from '@earendil-works/pi-ai';
2
+ import type { Config } from '../types';
3
+ import type { AgentDefinition } from './types';
4
+ import { type DecideFn } from './decisions';
5
+ export declare const BUTTRESS_PROVIDER_ID = "buttress";
6
+ /**
7
+ * Loopback requests never leave the process: this host name only exists so the
8
+ * OpenAI client has a syntactically valid base URL to resolve paths against.
9
+ * The injected fetch dispatches the request straight into the Elysia app.
10
+ */
11
+ export declare const LOOPBACK_BASE_URL = "http://buttress.internal/oai-compat/v1";
12
+ /** Fetch-shaped dispatcher into the local Elysia app (see loopback.ts). */
13
+ export type LoopbackFetch = typeof fetch;
14
+ /**
15
+ * A `Models` collection with every pi built-in provider (env-var API keys)
16
+ * plus the `buttress` pseudo-provider whose models are the configured LLM
17
+ * generators, streamed over the in-process OpenAI-compat loopback, and the
18
+ * configured decision generators as classifier models, answered by `decide`.
19
+ */
20
+ export declare const buildAgentModels: (config: Config, loopbackFetch: LoopbackFetch, decide: DecideFn) => MutableModels;
21
+ /** Resolve an agent's model reference against the registry, with clear errors. */
22
+ export declare const resolveAgentModel: (models: MutableModels, agent: AgentDefinition) => Model<any>;
@@ -0,0 +1,16 @@
1
+ import type { Config } from '../types';
2
+ import type { FunctionsService } from '../functions';
3
+ import type { FunctionRuntime } from '../functions/types';
4
+ import { type ElysiaHandle } from './loopback';
5
+ import { type AgentsConfig, type AgentsService } from './types';
6
+ export type CreateAgentsServiceOptions = {
7
+ config: Config;
8
+ agentsConfig: AgentsConfig;
9
+ /** Server runtime handed to function tool calls; `agents` is added here. */
10
+ runtimeBase: Omit<FunctionRuntime, 'agents'>;
11
+ /** Late-bound: the Elysia app exists only after the service (circular wiring). */
12
+ getHandle: () => ElysiaHandle | null;
13
+ /** Late-bound for the same reason (functions service needs the agents service). */
14
+ getFunctions: () => FunctionsService | null;
15
+ };
16
+ export declare const createAgentsService: ({ config, agentsConfig, runtimeBase, getHandle, getFunctions, }: CreateAgentsServiceOptions) => AgentsService;
@@ -0,0 +1,30 @@
1
+ import type { AgentMessage } from '@earendil-works/pi-agent-core';
2
+ import type { AgentSessionSummary, AgentsConfig } from './types';
3
+ type SessionMeta = {
4
+ id: string;
5
+ createdAt: number;
6
+ parentSessionId?: string;
7
+ path: string;
8
+ modifiedAt: number;
9
+ /** Written by pi-agent-core's v4 repo; converted on the first append. */
10
+ legacy: boolean;
11
+ };
12
+ export type AgentSession = {
13
+ metadata: SessionMeta;
14
+ /** True once the file is in the current format with no torn tail. */
15
+ appendable: boolean;
16
+ };
17
+ export type AgentSessionStore = {
18
+ create: (agentName: string) => Promise<AgentSession>;
19
+ /** Throws when the id is unknown within the agent's scope. */
20
+ open: (agentName: string, sessionId: string) => Promise<AgentSession>;
21
+ fork: (agentName: string, sessionId: string) => Promise<AgentSession>;
22
+ append: (session: AgentSession, message: AgentMessage) => Promise<void>;
23
+ list: (agentName: string, limit?: number) => Promise<AgentSessionSummary[]>;
24
+ /** Reconstruct the pi message history for continuing a session. */
25
+ messages: (session: AgentSession) => Promise<AgentMessage[]>;
26
+ /** Retention sweep across every configured agent scope. */
27
+ sweep: (agentNames: string[]) => Promise<number>;
28
+ };
29
+ export declare const createAgentSessionStore: (config: AgentsConfig) => AgentSessionStore;
30
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { FunctionsService } from '../functions';
2
+ import type { FunctionRuntime } from '../functions/types';
3
+ import type { AgentDefinition } from './types';
4
+ /**
5
+ * Local functions as agent tools. Each tool call funnels through the same
6
+ * executor as MCP/HTTP calls (lazy reload, per-call scratch dir, spawn
7
+ * tracking, the function's own deadline), and the run's abort signal chains
8
+ * in. Granting a function to an agent in config IS the authorization — no
9
+ * auth guard runs below the HTTP/MCP boundary.
10
+ */
11
+ export type FunctionToolContext = {
12
+ functions: FunctionsService;
13
+ runtime: FunctionRuntime;
14
+ /** Agent-invocation depth for calls made BY this run's tools. */
15
+ depth: number;
16
+ /**
17
+ * Forwards `context.emit` progress as agent events. `toolCallId` tells calls of
18
+ * one tool apart: pi runs the calls of a turn in parallel.
19
+ */
20
+ onToolEmit?: (toolName: string, event: string, data: unknown, toolCallId: string) => void;
21
+ };
22
+ export type AgentFunctionTool = {
23
+ name: string;
24
+ label: string;
25
+ description: string;
26
+ parameters: Record<string, any>;
27
+ execute: (toolCallId: string, args: any, signal?: AbortSignal) => Promise<any>;
28
+ };
29
+ /**
30
+ * Resolve the agent's configured tool list against the live registry.
31
+ * Fail-closed: a listed function that is missing (deleted, renamed, or the
32
+ * functions feature is off) rejects the run — a headless automation silently
33
+ * improvising around missing tools is worse than a loud failure.
34
+ */
35
+ export declare const buildFunctionTools: (agent: AgentDefinition, { functions, runtime, depth, onToolEmit }: FunctionToolContext) => Promise<AgentFunctionTool[]>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * pi-tui chat front-end for `bricks-buttress agent` — the interactive mode on
3
+ * a real terminal (line mode remains for pipes/scripts via --plain).
4
+ *
5
+ * Layout (main-screen mode, scrollback holds history):
6
+ * [chat log: user prompts, dim thinking, tool lines, markdown answers]
7
+ * [status line / spinner]
8
+ * [editor]
9
+ */
10
+ import { type Connection } from './client';
11
+ export type AgentTuiOptions = {
12
+ connection: Connection;
13
+ agentName: string;
14
+ sessionId: string | null;
15
+ fork: boolean;
16
+ };
17
+ export declare const runAgentTui: ({ connection, agentName, sessionId: initialSessionId, fork: initialFork, }: AgentTuiOptions) => Promise<void>;