@fugood/buttress-server 2.26.0-beta.3 → 2.26.0-beta.5

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
@@ -154,6 +154,7 @@ Configuration is loaded from a TOML file passed via `--config` / `-c`. Every top
154
154
  | `[openai_compat]` | Enable `/oai-compat/v1/*` — see [Compatibility Endpoints](#compatibility-endpoints-experimental) |
155
155
  | `[anthropic_messages]` | Enable `/anthropic-messages` — see [Compatibility Endpoints](#compatibility-endpoints-experimental)|
156
156
  | `[functions]` | Enable local functions — see [Local Functions](#local-functions-experimental) |
157
+ | `[[agents]]` | Config-defined agents — see [Agents](#agents-experimental) |
157
158
  | `[[generators]]` | Array of generator instances — one entry per loaded model |
158
159
 
159
160
  ### `[env]`
@@ -850,6 +851,117 @@ The handler receives `{ method, path, name?, headers, query, token, workspaceAut
850
851
 
851
852
  Function files themselves are trusted input, exactly like this config file. They run in a `node:vm` context with a clean global (no ambient `process` or `require`), but that is for clarity, not isolation — a function that is handed `spawn` can do anything the server process can. Only put code you wrote (or reviewed) in the functions directory.
852
853
 
854
+ ## Agents (Experimental)
855
+
856
+ Buttress can host **agents**: pi-based LLM loops that run inside the server
857
+ process, use **local functions** (and MCP servers) as their tools, and keep
858
+ **config-scoped sessions** on disk. The primary consumer is automation — a
859
+ local function or daemon calls `context.agents.run(...)` to add multi-step
860
+ reasoning to a server-side workflow; an interactive CLI exists for driving and
861
+ inspecting the same agents.
862
+
863
+ ```toml
864
+ [[agents]]
865
+ name = "ops-assistant" # unique; the session scope key
866
+ model = "buttress/ggml-org/gpt-oss-20b-GGUF" # split on the FIRST slash: provider/model-id
867
+ # model = "anthropic/claude-sonnet-5" # any pi-supported provider; API key from env
868
+ system_prompt_file = "./prompts/ops.md" # or inline: system_prompt = "..."
869
+ tools = ["get_server_status", "restart_service"] # local function names (explicit; no wildcard)
870
+ max_turns = 30 # assistant↔tool round-trips per run (default 30)
871
+ # max_tokens_per_run = 200000 # per-run token budget; unset = unlimited
872
+ # temperature = 0.2 # unrecognized keys pass through to generation
873
+
874
+ [agents.mcp_servers.github] # optional MCP servers (StreamableHTTP or stdio)
875
+ url = "https://api.githubcopilot.com/mcp/"
876
+ # headers = { Authorization = "Bearer ..." }
877
+ # optional = true # continue without this server if it won't connect
878
+
879
+ [agents_options] # optional; defaults shown
880
+ # sessions_dir = "./.buttress-agent/sessions" # relative to this config file
881
+ # session_max_age = "30d" # retention sweep; 0 disables
882
+ # session_max_count = 500 # per agent; 0 disables
883
+ # max_depth = 2 # function → agent → function → agent chain cap
884
+ # allow_unauthenticated = false # serve /agents on an UNBOUND server (see below)
885
+ ```
886
+
887
+ **Models.** `buttress/<repo_id>` targets a configured `[[generators]]` LLM
888
+ (ggml/mlx) and is validated at startup; traffic flows through an in-process
889
+ OpenAI-compat loopback (no socket, no extra config — `[openai_compat] enabled`
890
+ still only governs the external HTTP route). Any other provider prefix is a pi
891
+ built-in provider (`anthropic/…`, `openai/…`, `google/…`, …) authenticated by
892
+ its usual environment variable (`ANTHROPIC_API_KEY`, …) — set it via `[env]`
893
+ or the process environment. OAuth-based logins are not supported headless.
894
+
895
+ **Tools.** `tools` lists local function names explicitly. Each tool call runs
896
+ through the normal functions executor (same lazy reload, scratch dir, spawn
897
+ tracking, and the function's own `meta.timeout` deadline), and aborting the
898
+ run aborts in-flight tool calls and their spawned processes. A listed function
899
+ that is missing fails the run loudly rather than letting a headless automation
900
+ improvise around it. MCP tools get server-qualified names
901
+ (`mcp__github__create_issue`); MCP servers connect lazily on the first run and
902
+ fail closed unless marked `optional = true`.
903
+
904
+ **Sessions.** Each run returns a `sessionId`; pass it back to continue the
905
+ conversation, or add `fork: true` to branch it into a fresh session. Sessions
906
+ are JSONL files under `sessions_dir`, scoped by agent name (renaming an agent
907
+ orphans its sessions), written as the run streams — an aborted or timed-out
908
+ run still leaves a continuable transcript. Same-session runs queue; different
909
+ sessions run in parallel. A retention sweep prunes by age and count.
910
+
911
+ **From a local function** (the primary surface):
912
+
913
+ ```ts
914
+ export default async ({ service }, context) => {
915
+ const result = await context.agents.run('ops-assistant', {
916
+ prompt: `Investigate the '${service}' service and fix it if needed.`,
917
+ // sessionId, fork, onEvent are also accepted
918
+ })
919
+ return { conclusion: result.content, sessionId: result.sessionId }
920
+ }
921
+ ```
922
+
923
+ `context.agents.run` resolves within the caller's lifetime and deadline
924
+ (`context.signal` aborts it); long-running agent work belongs in a **daemon**
925
+ (no deadline) or a function with a raised `meta.timeout`. `context.agents.list()`
926
+ and `context.agents.sessions(name)` round out the surface. Chained invocations
927
+ (a tool function calling another agent) are capped by `agents_options.max_depth`.
928
+
929
+ **HTTP endpoints** (also what the CLI uses):
930
+
931
+ ```
932
+ GET /agents configured agent names
933
+ POST /agents/:name/run { prompt, sessionId?, fork? }; ?stream=1 for SSE
934
+ (a `session` event with the id arrives first,
935
+ then `agent` events, then `result`/`error`)
936
+ GET /agents/:name/sessions newest-first summaries (id, timestamps, preview)
937
+ GET /agents/:name/sessions/:id full transcript
938
+ POST /agents/:name/sessions/:id/abort abort the active run
939
+ ```
940
+
941
+ Auth mirrors the functions surface, not the open inference endpoints: a bound
942
+ server requires a workspace JWT; an **unbound server rejects remote calls**
943
+ unless `allow_unauthenticated = true`. The server also writes an ephemeral
944
+ internal token to `<sessions_dir>/../runtime-token` (mode 0600) at startup —
945
+ same-host CLIs authenticate with it automatically, bound or not.
946
+
947
+ **Interactive CLI**:
948
+
949
+ ```sh
950
+ bricks-buttress agent -c config.toml # list agents
951
+ bricks-buttress agent ops-assistant -c config.toml # chat (streams text, thinking, tool calls)
952
+ bricks-buttress agent ops-assistant --sessions -c config.toml
953
+ bricks-buttress agent ops-assistant --session <id> -c config.toml # continue
954
+ bricks-buttress agent ops-assistant --fork <id> -c config.toml # fork, then continue the fork
955
+ ```
956
+
957
+ On a real terminal the chat runs a pi-tui interface (markdown answers, dim
958
+ thinking, tool-call lines, Ctrl+C aborts the running turn); pipes/scripts —
959
+ or `--plain` — get a line-based renderer with identical semantics.
960
+
961
+ Like function files, agent definitions are trusted input: an agent is only as
962
+ safe as the functions and MCP servers you hand it.
963
+
964
+
853
965
  ## Session State Cache
854
966
 
855
967
  The server supports session state caching for ggml-llm generators, which saves KV cache state to disk after completions. This enables:
@@ -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
+ }
@@ -45,6 +45,28 @@ enabled = true
45
45
  # api_base = "https://example.internal"
46
46
  # api_keys = ["a-long-random-string"] # e.g. consumed by function-samples/_auth.ts
47
47
 
48
+ # Agents (EXPERIMENTAL): pi-based LLM loops running in this server, with local
49
+ # functions (and MCP servers) as tools and config-scoped sessions on disk.
50
+ # Local functions call them via context.agents.run(...); an interactive CLI
51
+ # (`bricks-buttress agent <name> -c <config>`) drives the same agents.
52
+ # See the "Agents" section of README.md.
53
+ # [[agents]]
54
+ # name = "ops-assistant"
55
+ # model = "buttress/ggml-org/gpt-oss-20b-GGUF" # provider/model-id, split on the FIRST slash
56
+ # system_prompt = "You are an ops automation agent. Use the provided tools."
57
+ # tools = ["host-info"] # local function names (explicit; no wildcard)
58
+ # max_turns = 30
59
+ # [agents.mcp_servers.example]
60
+ # url = "https://example.com/mcp/"
61
+ # optional = true
62
+
63
+ # [agents_options]
64
+ # sessions_dir = "./.buttress-agent/sessions"
65
+ # session_max_age = "30d" # retention; 0 disables
66
+ # session_max_count = 500 # per agent; 0 disables
67
+ # max_depth = 2 # function -> agent -> function chain cap
68
+ # allow_unauthenticated = false # serve /agents on an UNBOUND server
69
+
48
70
  [runtime]
49
71
  cache_dir = "./.buttress-cache"
50
72
  # huggingface_token = "hf_xx"
@@ -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,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,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,23 @@
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) => Promise<any>;
8
+ _mcpServer: string;
9
+ _mcpToolName: string;
10
+ };
11
+ /** `mcp__<server>__<tool>`, hashed into the cap when too long or colliding. */
12
+ export declare const qualifyToolName: (serverName: string, toolName: string, usedNames: Set<string>) => string;
13
+ export type McpManager = {
14
+ /**
15
+ * Tools for every configured server of an agent. Fail-closed: a server that
16
+ * won't connect rejects the whole call unless it is marked `optional`.
17
+ */
18
+ toolsFor: (agentName: string, servers: Record<string, AgentMcpServerConfig>) => Promise<McpAgentTool[]>;
19
+ dispose: () => Promise<void>;
20
+ };
21
+ export declare const createMcpManager: ({ configDir }: {
22
+ configDir: string;
23
+ }) => McpManager;
@@ -0,0 +1,20 @@
1
+ import { type Model, type MutableModels } from '@earendil-works/pi-ai';
2
+ import type { Config } from '../types';
3
+ import type { AgentDefinition } from './types';
4
+ export declare const BUTTRESS_PROVIDER_ID = "buttress";
5
+ /**
6
+ * Loopback requests never leave the process: this host name only exists so the
7
+ * OpenAI client has a syntactically valid base URL to resolve paths against.
8
+ * The injected fetch dispatches the request straight into the Elysia app.
9
+ */
10
+ export declare const LOOPBACK_BASE_URL = "http://buttress.internal/oai-compat/v1";
11
+ /** Fetch-shaped dispatcher into the local Elysia app (see loopback.ts). */
12
+ export type LoopbackFetch = typeof fetch;
13
+ /**
14
+ * A `Models` collection with every pi built-in provider (env-var API keys)
15
+ * plus the `buttress` pseudo-provider whose models are the configured LLM
16
+ * generators, streamed over the in-process OpenAI-compat loopback.
17
+ */
18
+ export declare const buildAgentModels: (config: Config, loopbackFetch: LoopbackFetch) => MutableModels;
19
+ /** Resolve an agent's model reference against the registry, with clear errors. */
20
+ 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,42 @@
1
+ import { FileError } from '@earendil-works/pi-agent-core';
2
+ import type { Result } from '@earendil-works/pi-agent-core';
3
+ /**
4
+ * Minimal FileSystem implementation for the jsonl session repo, over node:fs.
5
+ *
6
+ * pi's NodeExecutionEnv classifies fs failures with `error instanceof Error`,
7
+ * which breaks in vm-realm hosts (jest runs test code in a vm context, so
8
+ * node-core errors come from another realm and ENOENT stops mapping to
9
+ * not_found). This implementation reads `error.code` structurally — and also
10
+ * skips pulling the exec-capable execution env into the server for what is
11
+ * purely file storage.
12
+ */
13
+ type FileResult<T> = Promise<Result<T, FileError>>;
14
+ declare const toFileInfo: (target: string, stats: import('node:fs').Stats) => {
15
+ name: string;
16
+ path: string;
17
+ kind: "directory" | "file" | "symlink";
18
+ size: number;
19
+ mtimeMs: number;
20
+ };
21
+ /** The `Pick<FileSystem, …>` surface JsonlSessionRepo requires. */
22
+ export declare const createSessionFs: (rootDir: string) => {
23
+ absolutePath: (target: string) => FileResult<string>;
24
+ joinPath: (parts: string[]) => FileResult<string>;
25
+ readTextFile: (target: string) => FileResult<string>;
26
+ readTextLines: (target: string, options?: {
27
+ maxLines?: number;
28
+ }) => FileResult<string[]>;
29
+ writeFile: (target: string, content: string | Uint8Array) => FileResult<void>;
30
+ appendFile: (target: string, content: string | Uint8Array) => FileResult<void>;
31
+ renameFile: (source: string, destination: string) => FileResult<void>;
32
+ fileInfo: (target: string) => FileResult<ReturnType<typeof toFileInfo>>;
33
+ listDir: (target: string) => FileResult<ReturnType<typeof toFileInfo>[]>;
34
+ exists: (target: string) => FileResult<boolean>;
35
+ createDir: (target: string, options?: {
36
+ recursive?: boolean;
37
+ }) => FileResult<void>;
38
+ remove: (target: string, options?: {
39
+ recursive?: boolean;
40
+ }) => FileResult<void>;
41
+ };
42
+ export {};
@@ -0,0 +1,15 @@
1
+ import { Session } from '@earendil-works/pi-agent-core';
2
+ import type { AgentMessage } from '@earendil-works/pi-agent-core';
3
+ import type { AgentSessionSummary, AgentsConfig } from './types';
4
+ export type AgentSessionStore = {
5
+ create: (agentName: string) => Promise<Session<any>>;
6
+ /** Throws when the id is unknown within the agent's scope. */
7
+ open: (agentName: string, sessionId: string) => Promise<Session<any>>;
8
+ fork: (agentName: string, sessionId: string) => Promise<Session<any>>;
9
+ list: (agentName: string, limit?: number) => Promise<AgentSessionSummary[]>;
10
+ /** Reconstruct the pi message history for continuing a session. */
11
+ messages: (session: Session<any>) => Promise<AgentMessage[]>;
12
+ /** Retention sweep across every configured agent scope. */
13
+ sweep: (agentNames: string[]) => Promise<number>;
14
+ };
15
+ export declare const createAgentSessionStore: (config: AgentsConfig) => AgentSessionStore;
@@ -0,0 +1,32 @@
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
+ /** Forwards `context.emit` progress as agent events. */
17
+ onToolEmit?: (toolName: string, event: string, data?: unknown) => void;
18
+ };
19
+ export type AgentFunctionTool = {
20
+ name: string;
21
+ label: string;
22
+ description: string;
23
+ parameters: Record<string, any>;
24
+ execute: (toolCallId: string, args: any, signal?: AbortSignal) => Promise<any>;
25
+ };
26
+ /**
27
+ * Resolve the agent's configured tool list against the live registry.
28
+ * Fail-closed: a listed function that is missing (deleted, renamed, or the
29
+ * functions feature is off) rejects the run — a headless automation silently
30
+ * improvising around missing tools is worse than a loud failure.
31
+ */
32
+ 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>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Agent feature types: config-defined agents that run pi-agent-core loops
3
+ * inside the buttress-server process, with local functions (and MCP servers)
4
+ * as their tools and config-scoped JSONL sessions.
5
+ */
6
+ export type AgentMcpServerConfig = {
7
+ /** StreamableHTTP endpoint; mutually exclusive with `command`. */
8
+ url?: string;
9
+ headers?: Record<string, string>;
10
+ /** Stdio server command; mutually exclusive with `url`. */
11
+ command?: string;
12
+ args?: string[];
13
+ env?: Record<string, string>;
14
+ /** Degrade-and-continue when this server fails to connect (default: fail the run). */
15
+ optional?: boolean;
16
+ };
17
+ export type AgentDefinition = {
18
+ /** Unique agent name — the session scope key. */
19
+ name: string;
20
+ /** Model reference split on the FIRST slash: `provider/model-id`. */
21
+ provider: string;
22
+ modelId: string;
23
+ /** Raw `model` string from config, for error messages. */
24
+ modelRef: string;
25
+ systemPrompt: string | null;
26
+ /** Resolved absolute path; loaded lazily so edits apply per run. */
27
+ systemPromptFile: string | null;
28
+ /** Local function names exposed as tools (explicit list, no wildcard). */
29
+ tools: string[];
30
+ mcpServers: Record<string, AgentMcpServerConfig>;
31
+ /** Assistant↔tool round-trips per run. */
32
+ maxTurns: number;
33
+ /** Cumulative token budget per run; null = unlimited (default). */
34
+ maxTokensPerRun: number | null;
35
+ thinking?: 'off' | 'minimal' | 'low' | 'medium' | 'high';
36
+ /** Generation passthrough (temperature, top_p, …). */
37
+ generation: Record<string, unknown>;
38
+ };
39
+ export type AgentsConfig = {
40
+ agents: AgentDefinition[];
41
+ /** Directory the TOML config lives in (stdio MCP servers run with this cwd). */
42
+ configDir: string;
43
+ /** Absolute sessions root (default `./.buttress-agent/sessions` next to the config). */
44
+ sessionsDir: string;
45
+ /** Absolute path of the 0600 file holding the internal token for same-host CLIs. */
46
+ runtimeTokenFile: string;
47
+ sessionMaxAgeMs: number | null;
48
+ sessionMaxCount: number | null;
49
+ /** Agent-invocation chain depth cap (function → agent → function → agent…). */
50
+ maxDepth: number;
51
+ /** Serve /agents endpoints on an unbound server (default false — fail closed). */
52
+ allowUnauthenticated: boolean;
53
+ };
54
+ export type AgentUsage = {
55
+ input: number;
56
+ output: number;
57
+ cacheRead: number;
58
+ totalTurns: number;
59
+ };
60
+ export type AgentStopReason = 'end_turn' | 'max_turns' | 'token_budget' | 'aborted' | 'error';
61
+ export type AgentRunOptions = {
62
+ prompt: string;
63
+ /** Continue this session; omitted → new session. */
64
+ sessionId?: string;
65
+ /** With `sessionId`: fork it into a fresh session instead of continuing. */
66
+ fork?: boolean;
67
+ /** pi agent event passthrough (shape follows the pinned pi version). */
68
+ onEvent?: (event: unknown) => void;
69
+ /**
70
+ * Fires once the run's session exists (created, opened, or forked), before
71
+ * the model starts — the id to abort or continue with. A streaming client
72
+ * otherwise only learns it from the terminal event.
73
+ */
74
+ onSession?: (sessionId: string) => void;
75
+ /** Aborts the run (chains into tool calls and spawned processes). */
76
+ signal?: AbortSignal;
77
+ /** Internal: agent-invocation chain depth of the caller. */
78
+ depth?: number;
79
+ };
80
+ export type AgentRunResult = {
81
+ sessionId: string;
82
+ content: string;
83
+ reasoningContent?: string;
84
+ usage: AgentUsage;
85
+ stopReason: AgentStopReason;
86
+ };
87
+ export type AgentSessionSummary = {
88
+ sessionId: string;
89
+ createdAt: string;
90
+ updatedAt: string;
91
+ parentSession: string | null;
92
+ /** First user prompt (truncated) when available. */
93
+ preview: string | null;
94
+ };
95
+ export type AgentSessionListOptions = {
96
+ limit?: number;
97
+ };
98
+ /** Error whose `sessionId` survives a failed/aborted run (transcript is continuable). */
99
+ export declare class AgentRunError extends Error {
100
+ sessionId: string | null;
101
+ stopReason: AgentStopReason;
102
+ constructor(message: string, sessionId: string | null, stopReason?: AgentStopReason);
103
+ }
104
+ export type AgentsService = {
105
+ config: AgentsConfig;
106
+ /** Configured agent names. */
107
+ list: () => string[];
108
+ run: (name: string, options: AgentRunOptions) => Promise<AgentRunResult>;
109
+ sessions: (name: string, options?: AgentSessionListOptions) => Promise<AgentSessionSummary[]>;
110
+ /** Full transcript of one session (pi AgentMessage array). */
111
+ transcript: (name: string, sessionId: string) => Promise<unknown[]>;
112
+ /** Abort the active run on a session; true when one was running. */
113
+ abort: (name: string, sessionId: string) => boolean;
114
+ /** Constant-time check of the internal loopback/CLI token. */
115
+ verifyInternalToken: (token: string | null | undefined) => boolean;
116
+ dispose: () => Promise<void>;
117
+ };
118
+ /** The `context.agents` surface local functions receive. */
119
+ export type AgentsFunctionApi = {
120
+ run: (name: string, options: Omit<AgentRunOptions, 'signal' | 'depth'>) => Promise<AgentRunResult>;
121
+ list: () => string[];
122
+ sessions: (name: string, options?: AgentSessionListOptions) => Promise<AgentSessionSummary[]>;
123
+ };