@fugood/buttress-server 2.25.5 → 2.25.6

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 (47) hide show
  1. package/README.md +164 -4
  2. package/config/function-samples/README.md +2 -0
  3. package/config/function-samples/bank-note.ts +47 -0
  4. package/config/function-samples/bank-watch-daemon.ts +63 -0
  5. package/config/function-samples/run-agent.ts +39 -0
  6. package/config/sample.toml +22 -0
  7. package/lib/agent/cli.d.ts +19 -0
  8. package/lib/agent/client.d.ts +66 -0
  9. package/lib/agent/config.d.ts +15 -0
  10. package/lib/agent/context.d.ts +11 -0
  11. package/lib/agent/loopback.d.ts +21 -0
  12. package/lib/agent/mcp.d.ts +23 -0
  13. package/lib/agent/models.d.ts +20 -0
  14. package/lib/agent/service.d.ts +16 -0
  15. package/lib/agent/session-fs.d.ts +42 -0
  16. package/lib/agent/sessions.d.ts +15 -0
  17. package/lib/agent/tools.d.ts +32 -0
  18. package/lib/agent/tui.d.ts +17 -0
  19. package/lib/agent/types.d.ts +123 -0
  20. package/lib/cli-DrbWX4ea.mjs +22 -0
  21. package/lib/client-BCBBen9i.mjs +8 -0
  22. package/lib/config-lP89VahD.mjs +2 -0
  23. package/lib/functions/bank-subscribe.d.ts +46 -0
  24. package/lib/functions/bank.d.ts +21 -0
  25. package/lib/functions/daemons.d.ts +45 -0
  26. package/lib/functions/executor.d.ts +31 -4
  27. package/lib/functions/index.d.ts +17 -7
  28. package/lib/functions/registry.d.ts +7 -1
  29. package/lib/functions/status.d.ts +49 -1
  30. package/lib/functions/templates.d.ts +3 -1
  31. package/lib/functions/types.d.ts +129 -0
  32. package/lib/index.d.ts +8 -2
  33. package/lib/index.mjs +263 -48
  34. package/lib/mlx-bridge.py +681 -0
  35. package/lib/routes/agents.d.ts +37 -0
  36. package/lib/routes/anthropic-messages.d.ts +2 -2
  37. package/lib/routes/index.d.ts +1 -0
  38. package/lib/routes/openai-compat.d.ts +2 -2
  39. package/lib/tui-7B7x6A08.mjs +2 -0
  40. package/lib/types.d.ts +9 -0
  41. package/lib/utils/cors.check.d.ts +1 -0
  42. package/lib/utils/cors.d.ts +72 -0
  43. package/lib/utils/workspaceState.d.ts +9 -0
  44. package/package.json +9 -6
  45. package/public/status.html +77 -1
  46. package/public/lib/index.d.ts +0 -27
  47. package/public/lib/index.mjs +0 -110
@@ -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
+ };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import{a as e,i as t,n,r,t as i}from"./client-BCBBen9i.mjs";import a from"node:readline";const o=`\x1B[2m`,s=`\x1B[33m`,c=`\x1B[31m`,l=`\x1B[0m`,u=e=>{let t={name:null,configArg:null,url:null,token:null,sessionId:null,fork:!1,listSessions:!1,plain:!1};for(let n=0;n<e.length;n+=1){let r=e[n];r===`-h`||r===`--help`?(console.log(`
3
+ bricks-buttress agent — chat with a configured agent on a running server
4
+
5
+ Usage:
6
+ bricks-buttress agent [name] [options]
7
+
8
+ Options:
9
+ -c, --config <path|toml> Config file (finds server port + local token)
10
+ --url <url> Server base URL (default from config, else http://127.0.0.1:2080)
11
+ --token <token> Access token (default: the config's runtime token file)
12
+ --session <id> Continue an existing session
13
+ --fork <id> Fork a session, then continue the fork
14
+ --sessions List the agent's sessions and exit
15
+ --plain Line-based output (default when not a TTY)
16
+ -h, --help Show this help
17
+
18
+ In the chat: /exit quits, /new starts a fresh session, Ctrl+C aborts the
19
+ current run (a second Ctrl+C quits).
20
+ `),process.exit(0)):r===`-c`||r===`--config`?t.configArg=e[++n]??null:r===`--url`?t.url=e[++n]??null:r===`--token`?t.token=e[++n]??null:r===`--session`?t.sessionId=e[++n]??null:r===`--fork`?(t.sessionId=e[++n]??null,t.fork=!0):r===`--sessions`?t.listSessions=!0:r===`--plain`?t.plain=!0:!r.startsWith(`-`)&&!t.name?t.name=r:(console.error(`Unknown argument: ${r}`),process.exit(1))}return t},d=e=>{console.error(`${c}${e}${l}`),process.exit(1)},f=async(t,n,r,i,a,u)=>{let d=i,f=!1,p=!1,m=()=>{f&&=(process.stdout.write(`${l}\n`),!1)},h=await e(t,n,{prompt:r,sessionId:i??void 0,fork:a},{signal:u,onFrame:({event:e,payload:t})=>{if(e===`session`){t.sessionId&&t.sessionId!==d&&(d=t.sessionId,i||console.log(`${o}session ${d}${l}`));return}if(e!==`agent`)return;let n=t?.event;if(t?.type===`message_update`&&n)n.type===`thinking_delta`?(f||=(process.stdout.write(`${o}`),!0),process.stdout.write(n.delta??``),p=!0):n.type===`text_delta`&&(m(),process.stdout.write(n.delta??``),p=!0);else if(t?.type===`tool_execution_start`){m(),p&&process.stdout.write(`
21
+ `);let e=JSON.stringify(t.args??{});console.log(`${s}⚙ ${t.toolName}${l}${o}(${e.length>160?`${e.slice(0,160)}…`:e})${l}`),p=!1}else t?.type===`tool_execution_end`?console.log(t.isError?`${c}✗ ${t.toolName} failed${l}`:`${o}✓ ${t.toolName}${l}`):t?.type===`tool_emit`&&console.log(`${o} ${t.toolName} → ${t.event}${l}`)}});if(m(),h.kind===`result`){let e=h.result;d=e.sessionId??d;let t=e.usage||{};return process.stdout.write(`
22
+ `),console.log(`${o}— ${e.stopReason} · ${t.totalTurns??`?`} turn(s) · ${t.input??0} in / ${t.output??0} out · session ${d}${l}`),{sessionId:d,ok:!0}}return h.kind===`aborted`?(console.log(`\n${s}(aborted)${l}`),{sessionId:h.sessionId??d,ok:!1}):h.kind===`error`?(console.error(`\n${c}Run failed: ${h.message}${l}`),{sessionId:h.sessionId??d,ok:!1}):(console.error(`\n${c}Connection lost mid-run.${l}`),{sessionId:h.sessionId??d,ok:!1})},p=async(e,t,n,r)=>{let s=n,u=r;console.log(`${t}${l} @ ${e.baseUrl}`+(s?` ${o}(${u?`forking`:`continuing`} ${s})${l}`:``)),console.log(`${o}/exit to quit, /new for a fresh session, Ctrl+C aborts a running turn${l}`);let d=a.createInterface({input:process.stdin,output:process.stdout}),p=!1;d.on(`close`,()=>{p=!0});let m=e=>new Promise(t=>{if(p){t(null);return}d.question(e,t),d.once(`close`,()=>t(null))}),h=null;for(d.on(`SIGINT`,()=>{h?(h.abort(),h=null):(d.close(),process.exit(0))});;){let n=await m(`> ${l}`);if(n==null)break;let r=n.trim();if(!r)continue;if(r===`/exit`||r===`/quit`)break;if(r===`/new`){s=null,u=!1,console.log(`${o}Started a fresh session.${l}`);continue}h=new AbortController;let a=s;try{let n=await f(e,t,r,s,u,h.signal);n.sessionId&&(s=n.sessionId),i(a,s)&&(u=!1)}catch(e){console.error(`${c}${e?.message||e}${l}`)}finally{h=null}}d.close()},m=async e=>{let i=u(e),{config:a,agentsConfig:s}=n(i.configArg),c=a.server.port||2080,f=(i.url||`http://127.0.0.1:${c}`).replace(/\/$/,``),m={baseUrl:f,token:i.token||(s?r(s.runtimeTokenFile):null)||process.env.BUTTRESS_AGENT_TOKEN||null},h=await t(m,`/agents`).catch(e=>d(`Cannot reach ${f}: ${e?.message||e} (is the server running?)`));if(!h.ok){let e=await h.text().catch(()=>``);d(`Server rejected the request (${h.status}): ${e.slice(0,300)}`)}let{agents:g}=await h.json();if(!i.name){console.log(`Configured agents on ${f}:`);for(let e of g)console.log(`- ${e}`);g.length===0&&console.log(`(none — add [[agents]] to the server config)`);return}if(g.includes(i.name)||d(`Unknown agent "${i.name}" (configured: ${g.join(`, `)||`none`})`),i.listSessions){let{sessions:e}=await(await t(m,`/agents/${encodeURIComponent(i.name)}/sessions`)).json();if(!e?.length){console.log(`No sessions yet.`);return}for(let t of e)console.log(`${t.sessionId} ${o}${t.updatedAt}${l}${t.parentSession?` ${o}(fork of ${t.parentSession})${l}`:``}`),t.preview&&console.log(` ${o}${t.preview}${l}`);return}if(process.stdin.isTTY&&process.stdout.isTTY&&!i.plain){let{runAgentTui:e}=await import(`./tui-7B7x6A08.mjs`);await e({connection:m,agentName:i.name,sessionId:i.sessionId,fork:i.fork});return}await p(m,i.name,i.sessionId,i.fork)};export{m as runAgentCommand};
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import{a as e,n as t}from"./config-lP89VahD.mjs";import n from"node:path";import r from"node:fs";import i from"@iarna/toml";const a=a=>{let o={},s=process.cwd();a&&(r.existsSync(a)?(o=i.parse(r.readFileSync(a,`utf8`)),s=n.dirname(n.resolve(a))):o=i.parse(a));let c=e(o);return{config:c,configDir:s,agentsConfig:t(c,{configDir:s})}},o=e=>{try{return r.readFileSync(e,`utf8`).trim()||null}catch{return null}},s=async(e,t,n)=>fetch(`${e.baseUrl}${t}`,{...n,headers:{"content-type":`application/json`,...e.token?{authorization:`Bearer ${e.token}`}:{},...n?.headers}});async function*c(e){let t=e.getReader(),n=new TextDecoder,r=``;try{for(;;){let{done:e,value:i}=await t.read();if(e)break;r+=n.decode(i,{stream:!0});let a=r.indexOf(`
3
+
4
+ `);for(;a!==-1;){let e=r.slice(0,a);r=r.slice(a+2),a=r.indexOf(`
5
+
6
+ `);let t=`message`,n=[];for(let r of e.split(`
7
+ `))r.startsWith(`event:`)?t=r.slice(6).trim():r.startsWith(`data:`)&&n.push(r.slice(5).trimStart());n.length>0&&(yield{event:t,data:n.join(`
8
+ `)})}}}finally{t.releaseLock()}}const l=(e,t)=>t!=null&&t!==e,u=async(e,t,n,{signal:r,onFrame:i})=>{let a=n.sessionId??null,o;try{o=await s(e,`/agents/${encodeURIComponent(t)}/run?stream=1`,{method:`POST`,body:JSON.stringify({prompt:n.prompt,...n.sessionId?{sessionId:n.sessionId}:{},...n.fork?{fork:!0}:{}}),signal:r})}catch(e){if(r?.aborted)return{kind:`aborted`,sessionId:a};throw e}if(!o.ok||!o.body){let e=await o.text().catch(()=>``);return{kind:`error`,message:`Run request failed (${o.status}): ${e.slice(0,400)}`,sessionId:a}}try{for await(let e of c(o.body)){let t;try{t=JSON.parse(e.data)}catch{continue}if(e.event===`session`&&t?.sessionId&&(a=t.sessionId),e.event===`result`)return i({event:e.event,payload:t}),{kind:`result`,result:t};if(e.event===`error`)return i({event:e.event,payload:t}),{kind:`error`,message:t?.message||`run failed`,sessionId:t?.sessionId??a};i({event:e.event,payload:t})}}catch(e){if(r?.aborted)return{kind:`aborted`,sessionId:a};throw e}return r?.aborted?{kind:`aborted`,sessionId:a}:{kind:`disconnected`,sessionId:a}};export{u as a,s as i,a as n,o as r,l as t};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import e from"node:path";import t from"node:os";import n from"bytes";import r from"ms";import i from"node-machine-id";const a=(e={},t={})=>{let n=Array.isArray(e)?[...e]:{...e};return Object.entries(t||{}).forEach(([e,t])=>{t&&typeof t==`object`&&!Array.isArray(t)?n[e]=a(n[e]||{},t):n[e]=t}),n},o=e=>e&&typeof e==`object`?structuredClone(e):null,s=(e,t)=>{let n=o(e)||{},r=o(t)||{};return a(n,r)},c=(e,t)=>a(structuredClone(e.global),t||{}),l=(e,t,n,r)=>{if(e.generators.length>0){let i=e.generators.filter(e=>e?.type===n);if(i.length>0&&r){let a=i.find(e=>t.getModelIdentifier(n,e)===r);if(a)return c(e,a)}}return Object.keys(e.global).length>0?c(e,{}):null},u={udp:{port:8089,announcements:{enabled:!0,interval:5e3},requests:{enabled:!0,responseDelay:100}},http:{enabled:!0,path:`/buttress/info`,cors:!0}},d=e=>e?e===!0?{...u}:a(u,e):null,f=(e,t)=>{if(!e.generators||e.generators.length===0)return t.map(e=>({type:e}));let n=new Set;return e.generators.forEach(e=>{e.type&&n.add(e.type)}),n.size===0?t.map(e=>({type:e})):Array.from(n).map(e=>({type:e}))},p=(e,t,n)=>e===void 0?n:typeof e==`number`?e:t(e)??n,m=6e4,h=1024*1024*50,g=s=>{let c=i.machineIdSync(),l={server:{id:`buttress-${c}`,name:`Buttress Server (${c.slice(-8)})`,port:2080,temp_file_dir:e.join(t.tmpdir(),`.buttress`),session_timeout:m,max_body_size:h},autodiscover:!1},u=a(l,o(s)||{}),f=Array.isArray(u.generators)?u.generators:[],{server:g,generators:_,autodiscover:v,...y}=u;return{autodiscover:d(v),server:{id:g.id,name:g.name,port:g.port,log_level:g.log_level,temp_file_dir:g.temp_file_dir,max_body_size:p(g.max_body_size,n.parse,h),session_timeout:p(g.session_timeout,e=>r(e),m)},global:y,generators:f}},_=/^[A-Za-z0-9_-]{1,64}$/,v=n=>n===`~`||n.startsWith(`~/`)?e.join(t.homedir(),n.slice(1)):n;var y=class extends Error{constructor(e){super(`[Agents] ${e}`),this.name=`AgentsConfigError`}};const b=(e,t,n)=>{if(t==null)return n;if(t===0||t===!1)return null;if(typeof t==`number`){if(Number.isFinite(t)&&t>0)return t}else if(typeof t==`string`){let e;try{e=r(t)}catch{e=void 0}if(typeof e==`number`&&e>0)return e}throw new y(`agents_options.${e} must be a duration string (e.g. "30d") or a positive number of milliseconds; use 0 to disable it`)},x=(e,t)=>{if(t==null)return{};if(typeof t!=`object`||Array.isArray(t))throw new y(`agent "${e}": mcp_servers must be a table`);let n={};for(let[r,i]of Object.entries(t)){if(!_.test(r))throw new y(`agent "${e}": MCP server name "${r}" must match ${_}`);let t=!!(typeof i?.url==`string`&&i.url),a=!!(typeof i?.command==`string`&&i.command);if(t===a)throw new y(`agent "${e}": MCP server "${r}" needs exactly one of url / command`);n[r]={url:t?i.url:void 0,headers:i?.headers&&typeof i.headers==`object`?i.headers:void 0,command:a?i.command:void 0,args:Array.isArray(i?.args)?i.args.map(String):void 0,env:i?.env&&typeof i.env==`object`?i.env:void 0,optional:i?.optional===!0}}return n},S=(e,t,n)=>{let r=e?.name;if(typeof r!=`string`||!_.test(r))throw new y(`[[agents]] #${t+1}: name is required and must match ${_}`);let i=e?.model;if(typeof i!=`string`||!i.includes(`/`))throw new y(`agent "${r}": model must be "provider/model-id" (e.g. "buttress/ggml-org/gpt-oss-20b-GGUF")`);let a=i.indexOf(`/`),o=i.slice(0,a),s=i.slice(a+1);if(!o||!s)throw new y(`agent "${r}": invalid model reference "${i}"`);if(o===`buttress`&&!n.generators.filter(e=>e.type===`ggml-llm`||e.type===`mlx-llm`).some(e=>(e.model?.repo_id||e.model?.repository)===s))throw new y(`agent "${r}": model "${s}" does not match any configured [[generators]] ggml-llm/mlx-llm repo_id`);if(e?.system_prompt!=null&&e?.system_prompt_file!=null)throw new y(`agent "${r}": set system_prompt or system_prompt_file, not both`);let c=[];if(e?.tools!=null){if(!Array.isArray(e.tools))throw new y(`agent "${r}": tools must be an array of function names`);for(let t of e.tools){if(typeof t!=`string`||!_.test(t))throw new y(`agent "${r}": tool name "${t}" must match ${_} (local function names only)`);if(c.includes(t))throw new y(`agent "${r}": duplicate tool "${t}"`);c.push(t)}}let l=Number(e?.max_turns??30);if(!Number.isInteger(l)||l<1)throw new y(`agent "${r}": max_turns must be a positive integer`);let u=null;if(e?.max_tokens_per_run!=null&&(u=Number(e.max_tokens_per_run),!Number.isInteger(u)||u<1))throw new y(`agent "${r}": max_tokens_per_run must be a positive integer`);let{name:d,model:f,system_prompt:p,system_prompt_file:m,tools:h,mcp_servers:g,max_turns:v,max_tokens_per_run:b,thinking:S,...C}=e;return{name:r,provider:o,modelId:s,modelRef:i,systemPrompt:typeof p==`string`?p:null,systemPromptFile:typeof m==`string`?m:null,tools:c,mcpServers:x(r,g),maxTurns:l,maxTokensPerRun:u,thinking:typeof S==`string`?S:void 0,generation:C}},C=(t,{configDir:n=process.cwd()}={})=>{let r=t.global||{},i=r.agents;if(i==null)return null;if(!Array.isArray(i))throw new y(`agents must be an array of tables ([[agents]])`);if(i.length===0)return null;let a=i.map((e,n)=>S(e,n,t)),o=new Set;for(let e of a){if(o.has(e.name))throw new y(`duplicate agent name "${e.name}"`);o.add(e.name)}let s=r.agents_options||{},c=typeof s.sessions_dir==`string`&&s.sessions_dir?s.sessions_dir:`./.buttress-agent/sessions`,l=v(c),u=e.isAbsolute(l)?l:e.resolve(n,l);for(let t of a)if(t.systemPromptFile){let r=v(t.systemPromptFile);t.systemPromptFile=e.isAbsolute(r)?r:e.resolve(n,r)}let d=Number(s.max_depth??2);if(!Number.isInteger(d)||d<1)throw new y(`agents_options.max_depth must be a positive integer`);let f=s.session_max_count,p=500;if(f===0||f===!1)p=null;else if(f!=null&&(p=Number(f),!Number.isInteger(p)||p<1))throw new y(`agents_options.session_max_count must be a positive integer`);return{agents:a,configDir:n,sessionsDir:u,runtimeTokenFile:e.join(e.dirname(u),`runtime-token`),sessionMaxAgeMs:b(`session_max_age`,s.session_max_age,2592e6),sessionMaxCount:p,maxDepth:d,allowUnauthenticated:s.allow_unauthenticated===!0}};export{g as a,o as i,C as n,l as o,s as r,f as s,y as t};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Data Bank change subscriptions for daemon functions.
3
+ *
4
+ * The Bank's `/api/subscriptions` WebSocket speaks the *legacy* `graphql-ws`
5
+ * protocol (`connection_init` → `connection_ack`, `start` → `data`, `ka`
6
+ * keep-alives) — that is what the deployed juniper handler serves, and what
7
+ * every production client falls back to. The vocabulary is four message types,
8
+ * so this is a small hand-rolled client rather than a protocol library (the
9
+ * maintained `graphql-ws` npm package only speaks the newer
10
+ * `graphql-transport-ws` protocol, which the Bank does not).
11
+ *
12
+ * Auth rides on the upgrade request's query string (`?spacename=&spacekey=`),
13
+ * which the Bank accepts as an alternative to headers — the standard
14
+ * `WebSocket` global cannot set custom headers.
15
+ *
16
+ * One connection multiplexes every daemon's subscription (one `start` per
17
+ * daemon, ids routed on `data`). The socket opens when the first subscription
18
+ * is added, reconnects with backoff while any remain, and closes when the
19
+ * last one is removed. The Bank pushes `ka` every ~10s, so a silent socket is
20
+ * a dead one: a watchdog forces a reconnect when nothing arrives for a while.
21
+ */
22
+ import type { BankBinding } from '../utils/workspaceState';
23
+ import type { BankProperty } from './types';
24
+ export type BankSubscriberStatus = 'idle' | 'connecting' | 'connected' | 'disconnected';
25
+ export type BankSubscription = {
26
+ close: () => void;
27
+ };
28
+ export type BankSubscriberOptions = {
29
+ /** Injectable for tests; defaults to the global WebSocket. */
30
+ webSocketImpl?: typeof WebSocket;
31
+ /** Injectable timer fns for tests. */
32
+ setTimeoutFn?: typeof setTimeout;
33
+ clearTimeoutFn?: typeof clearTimeout;
34
+ };
35
+ /** Build the legacy-protocol subscriptions URL for a Bank binding. */
36
+ export declare const bankSubscriptionsUrl: (binding: BankBinding) => string;
37
+ export type BankSubscriber = ReturnType<typeof createBankSubscriber>;
38
+ export declare const createBankSubscriber: (binding: BankBinding, { webSocketImpl, setTimeoutFn, clearTimeoutFn, }?: BankSubscriberOptions) => {
39
+ status: () => BankSubscriberStatus;
40
+ /**
41
+ * Watch a set of property ids. `onProperties` receives each change batch;
42
+ * `onStatus` follows the shared connection's health.
43
+ */
44
+ subscribe(filteredProps: string[], onProperties: (properties: BankProperty[]) => void, onStatus: (status: BankSubscriberStatus) => void): BankSubscription;
45
+ dispose(): void;
46
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `context.bank` — remote Data Bank access for local functions.
3
+ *
4
+ * Talks to the public Data Bank GraphQL API (SPACENAME/SPACEKEY headers) with
5
+ * credentials issued by `bricks buttress bank-key` and stored in the buttress
6
+ * state file. Read/write only in v1 — no subscription surface. Mirrors the
7
+ * client in packages/bricks-cli/src/utils/bank-client.js.
8
+ */
9
+ import type { BankContext, BankProperty } from './types';
10
+ import type { BankBinding } from '../utils/workspaceState';
11
+ export declare const PROPERTY_FIELDS = "\n propertyId\n meta\n definition\n value\n tags\n lastUpdateHash\n lastUpdateNote\n lastUpdateKey\n createAt\n updateAt\n";
12
+ export declare const normalizeProperty: (property: any) => BankProperty;
13
+ export declare class BankNotConfiguredError extends Error {
14
+ constructor();
15
+ }
16
+ type CreateBankContextOptions = {
17
+ /** Ends in-flight Bank requests when the call is aborted or times out. */
18
+ signal: AbortSignal;
19
+ };
20
+ export declare const createBankContext: (binding: BankBinding | null | undefined, { signal }: CreateBankContextOptions) => BankContext;
21
+ export {};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Daemon functions: long-lived local functions.
3
+ *
4
+ * A file with `meta.daemon = true` is a daemon. Its default export runs
5
+ * **once** when the daemon starts, with a `DaemonContext` — the ordinary
6
+ * function context plus lifetime APIs — and everything it registers there
7
+ * keeps running after the invocation returns:
8
+ *
9
+ * - `context.setInterval(cb, ms)` / `context.clearInterval(handle)` —
10
+ * managed timers; a callback that throws is logged, never fatal.
11
+ * - `context.bank.subscribe(ids, onChange)` — remote Data Bank change
12
+ * notifications over one shared, auto-reconnecting connection.
13
+ * - `context.onEvent(handler)` — events other local functions send via
14
+ * `context.daemons.emit(name, event, data)`.
15
+ *
16
+ * No deadline applies to a daemon (`meta.timeout` is ignored). It stops when
17
+ * its file changes or disappears (the manager's reconcile — eager with the
18
+ * hot-reload watcher, a periodic sweep otherwise) or on server shutdown:
19
+ * `context.signal` aborts, timers and subscriptions are torn down, spawned
20
+ * children are killed, and a cleanup function returned by the handler (if
21
+ * any) gets a bounded run. A file edit then starts a fresh module.
22
+ */
23
+ import { createBankSubscriber } from './bank-subscribe';
24
+ import type { FunctionsRegistry } from './registry';
25
+ import type { FunctionsStatusTracker } from './status';
26
+ import type { DaemonSummary, DaemonsHub, FunctionRuntime, FunctionsConfig } from './types';
27
+ export type CreateDaemonManagerOptions = {
28
+ registry: FunctionsRegistry;
29
+ runtime: FunctionRuntime;
30
+ functionsConfig: FunctionsConfig;
31
+ tracker?: FunctionsStatusTracker;
32
+ /** Injectable for tests; defaults to `createBankSubscriber`. */
33
+ subscriberFactory?: typeof createBankSubscriber;
34
+ reconcileIntervalMs?: number;
35
+ /** Called after every reconcile with the current daemon count. */
36
+ onReconciled?: (count: number) => void;
37
+ };
38
+ export type DaemonManager = ReturnType<typeof createDaemonManager>;
39
+ export declare const createDaemonManager: ({ registry, runtime, functionsConfig, tracker, subscriberFactory, reconcileIntervalMs, onReconciled, }: CreateDaemonManagerOptions) => {
40
+ hub: DaemonsHub;
41
+ reconcile: () => Promise<void>;
42
+ count: () => number;
43
+ list: () => DaemonSummary[];
44
+ dispose: () => Promise<void>;
45
+ };
@@ -8,20 +8,47 @@
8
8
  * synchronously cannot be interrupted — function files are trusted, and vm has
9
9
  * no way to preempt a running script.
10
10
  */
11
- import type { FunctionEmit, FunctionRuntime, FunctionsConfig, LoadedFunction } from './types';
11
+ import type { DaemonsHub, FunctionContext, FunctionEmit, FunctionRuntime, FunctionsConfig, LoadedFunction } from './types';
12
12
  export declare class FunctionTimeoutError extends Error {
13
13
  constructor(name: string, timeoutMs: number);
14
14
  }
15
15
  export declare class FunctionAbortError extends Error {
16
16
  constructor(name: string);
17
17
  }
18
- export type ExecuteOptions = {
18
+ export type CallContextOptions = {
19
19
  runtime: FunctionRuntime;
20
20
  functionsConfig: FunctionsConfig;
21
21
  /** Streams progress to the caller; ignored on non-streaming surfaces. */
22
22
  emit?: FunctionEmit;
23
+ callId?: string;
24
+ /** Routes `context.daemons`; absent when the daemon manager is not running. */
25
+ daemons?: DaemonsHub;
26
+ /**
27
+ * Agent-invocation chain depth of this call (0 = a person/HTTP/MCP caller).
28
+ * Agent-invoked tool calls pass depth+1 so `context.agents.run` can cap
29
+ * function -> agent -> function -> agent recursion.
30
+ */
31
+ agentDepth?: number;
32
+ };
33
+ export type ExecuteOptions = CallContextOptions & {
23
34
  /** Aborts the call early (e.g. the HTTP client disconnected). */
24
35
  signal?: AbortSignal;
25
- callId?: string;
26
36
  };
27
- export declare const executeFunction: (fn: LoadedFunction, input: any, { runtime, functionsConfig, emit, signal, callId }: ExecuteOptions) => Promise<any>;
37
+ /** A live function context plus the levers that end it. */
38
+ export type FunctionCallHandle = {
39
+ context: FunctionContext;
40
+ /** Aborted when the run is stopped or times out. */
41
+ signal: AbortSignal;
42
+ /** Abort the run and terminate every child process it spawned. */
43
+ abort: () => void;
44
+ /** Final cleanup: stop `emit` delivery and kill leftover children. */
45
+ finish: () => void;
46
+ };
47
+ /**
48
+ * Build the context a function runs against, without any deadline or
49
+ * lifecycle policy. `executeFunction` wraps this for ordinary calls (deadline
50
+ * + abort race); the daemon manager uses it directly for its long-lived
51
+ * runs, where the context must outlive the initial invocation.
52
+ */
53
+ export declare const createCallContext: (fn: LoadedFunction, { runtime, functionsConfig, emit, callId, daemons, agentDepth, }: CallContextOptions) => FunctionCallHandle;
54
+ export declare const executeFunction: (fn: LoadedFunction, input: any, { signal, ...contextOptions }: ExecuteOptions) => Promise<any>;
@@ -6,9 +6,9 @@
6
6
  * owns discovery/reload (registry) and execution (executor), and prepares the
7
7
  * directory for authoring on startup.
8
8
  */
9
- import type { FunctionEmit, FunctionRuntime, FunctionSummary, FunctionsConfig, LoadedAuthFunction } from './types';
9
+ import type { DaemonSummary, FunctionEmit, FunctionRuntime, FunctionSummary, FunctionsConfig, LoadedAuthFunction } from './types';
10
10
  export { resolveFunctionsConfig } from './config';
11
- export { FunctionNotFoundError } from './registry';
11
+ export { FunctionNotFoundError, FunctionNotCallableError } from './registry';
12
12
  export { FunctionAbortError, FunctionTimeoutError } from './executor';
13
13
  export { FunctionImportError } from './loader';
14
14
  export { AUTH_BASENAME } from './auth';
@@ -19,12 +19,16 @@ export type CallOptions = {
19
19
  signal?: AbortSignal;
20
20
  callId?: string;
21
21
  /** Which API carried the call — recorded in the status history. */
22
- surface?: 'http' | 'sse' | 'mcp';
22
+ surface?: 'http' | 'sse' | 'mcp' | 'agent';
23
+ /** Agent-invocation chain depth (agent tool calls pass their run's depth). */
24
+ agentDepth?: number;
23
25
  };
24
26
  export type FunctionsService = {
25
27
  config: FunctionsConfig;
26
28
  dir: string;
27
29
  list: () => Promise<FunctionSummary[]>;
30
+ /** Daemons the manager is currently running (empty when daemons are off). */
31
+ listDaemons: () => DaemonSummary[];
28
32
  /** One function's summary; rejects when it is unknown or fails to load. */
29
33
  describe: (name: string) => Promise<FunctionSummary>;
30
34
  call: (name: string, input: any, options: CallOptions) => Promise<any>;
@@ -35,18 +39,24 @@ export type FunctionsService = {
35
39
  getCustomAuth: () => Promise<LoadedAuthFunction | null>;
36
40
  /**
37
41
  * Live capability summary for `serverInfo`. Mutated in place on every
38
- * `list()` so the announced count follows the directory instead of freezing
39
- * at whatever was on disk during startup.
42
+ * `list()` (and every daemon reconcile) so the announced counts follow the
43
+ * directory instead of freezing at whatever was on disk during startup.
40
44
  */
41
45
  stats: {
42
46
  enabled: true;
43
47
  count: number;
48
+ daemons: number;
44
49
  };
45
- /** Stop the hot-reload watcher, when one is running. Safe to call always. */
50
+ /** Stop the hot-reload watcher and daemons. Safe to call always. */
46
51
  dispose: () => void;
47
52
  };
48
53
  export type CreateFunctionsServiceOptions = {
49
54
  /** `server.temp_file_dir`; per-call scratch space lives under it. */
50
55
  tempFileDir: string;
56
+ /**
57
+ * Server-wide runtime for daemon invocations. Daemon files are inert (with
58
+ * a warning) when omitted — request surfaces still pass a runtime per call.
59
+ */
60
+ runtime?: FunctionRuntime;
51
61
  };
52
- export declare const createFunctionsService: (config: FunctionsConfig, { tempFileDir }: CreateFunctionsServiceOptions) => Promise<FunctionsService>;
62
+ export declare const createFunctionsService: (config: FunctionsConfig, { tempFileDir, runtime }: CreateFunctionsServiceOptions) => Promise<FunctionsService>;
@@ -10,7 +10,7 @@
10
10
  * re-transpiled into a fresh vm context. No watchers, identical behavior on
11
11
  * every platform, and an edit is picked up by the very next call.
12
12
  */
13
- import type { FunctionSummary, FunctionsConfig, LoadedFunction } from './types';
13
+ import type { DaemonSummary, FunctionSummary, FunctionsConfig, LoadedFunction } from './types';
14
14
  type Entry = {
15
15
  name: string;
16
16
  file: string;
@@ -22,6 +22,9 @@ type Entry = {
22
22
  export declare class FunctionNotFoundError extends Error {
23
23
  constructor(name: string);
24
24
  }
25
+ export declare class FunctionNotCallableError extends Error {
26
+ constructor(name: string);
27
+ }
25
28
  export type FunctionsRegistry = ReturnType<typeof createFunctionsRegistry>;
26
29
  export declare const createFunctionsRegistry: (config: FunctionsConfig) => {
27
30
  dir: string;
@@ -30,6 +33,9 @@ export declare const createFunctionsRegistry: (config: FunctionsConfig) => {
30
33
  get: (name: string) => Promise<LoadedFunction>;
31
34
  describe: (name: string) => Promise<FunctionSummary>;
32
35
  list: () => Promise<FunctionSummary[]>;
36
+ listDaemons: () => Promise<LoadedFunction[]>;
33
37
  entries: Map<string, Entry>;
34
38
  };
39
+ /** A daemon's public shape, as reported by `GET /functions` and the status page. */
40
+ export declare const toDaemonSummary: (fn: LoadedFunction) => DaemonSummary;
35
41
  export {};
@@ -13,11 +13,41 @@
13
13
  export type FunctionCallRecord = {
14
14
  timestamp: string;
15
15
  name: string;
16
- surface: 'http' | 'sse' | 'mcp';
16
+ surface: 'http' | 'sse' | 'mcp' | 'agent';
17
17
  durationMs: number;
18
18
  success: boolean;
19
19
  error?: string;
20
20
  };
21
+ export type DaemonActivityRecord = {
22
+ timestamp: string;
23
+ name: string;
24
+ /** Which daemon callback ran ('start', 'interval', 'bank', 'event', 'stop'). */
25
+ event: string;
26
+ durationMs: number;
27
+ success: boolean;
28
+ error?: string;
29
+ };
30
+ /** Live state of one daemon, provided by the daemon manager. */
31
+ export type DaemonStateSnapshot = {
32
+ name: string;
33
+ state: 'running' | 'error';
34
+ description: string;
35
+ startedAt: string | null;
36
+ /** Active `context.setInterval` timers. */
37
+ timers: number;
38
+ /** Active `context.bank.subscribe` subscriptions. */
39
+ bankSubscriptions: number;
40
+ /** Data Bank connection health; null while the daemon holds no subscriptions. */
41
+ bank: 'connected' | 'connecting' | 'disconnected' | null;
42
+ /** Whether the daemon registered a `context.onEvent` handler. */
43
+ listening: boolean;
44
+ counts: {
45
+ runs: number;
46
+ failed: number;
47
+ };
48
+ /** Why the daemon is in 'error' state (start failed / file no longer loads). */
49
+ error?: string;
50
+ };
21
51
  export type FunctionUploadRecord = {
22
52
  timestamp: string;
23
53
  /** 'upload' = POST /functions/upload, 'call' = multipart function call. */
@@ -51,6 +81,9 @@ export declare const createFunctionsStatusTracker: (maxHistory?: number) => {
51
81
  recordCall(record: Omit<FunctionCallRecord, 'timestamp'>): void;
52
82
  recordUpload(record: Omit<FunctionUploadRecord, 'timestamp'>): void;
53
83
  recordDownload(record: Omit<FunctionDownloadRecord, 'timestamp'>): void;
84
+ recordDaemonActivity(record: Omit<DaemonActivityRecord, 'timestamp'>): void;
85
+ /** Register (or clear, with null) the daemon manager's live-state source. */
86
+ setDaemonsProvider(provider: (() => DaemonStateSnapshot[]) | null): void;
54
87
  recordAuth(record: Omit<FunctionAuthRecord, 'timestamp'>): void;
55
88
  snapshot(): {
56
89
  counters: {
@@ -72,12 +105,18 @@ export declare const createFunctionsStatusTracker: (maxHistory?: number) => {
72
105
  total: number;
73
106
  denied: number;
74
107
  };
108
+ daemons: {
109
+ invocations: number;
110
+ failed: number;
111
+ };
75
112
  };
113
+ daemons: DaemonStateSnapshot[];
76
114
  history: {
77
115
  calls: FunctionCallRecord[];
78
116
  uploads: FunctionUploadRecord[];
79
117
  downloads: FunctionDownloadRecord[];
80
118
  auth: FunctionAuthRecord[];
119
+ daemons: DaemonActivityRecord[];
81
120
  };
82
121
  };
83
122
  };
@@ -87,6 +126,9 @@ export declare const functionsStatusTracker: {
87
126
  recordCall(record: Omit<FunctionCallRecord, 'timestamp'>): void;
88
127
  recordUpload(record: Omit<FunctionUploadRecord, 'timestamp'>): void;
89
128
  recordDownload(record: Omit<FunctionDownloadRecord, 'timestamp'>): void;
129
+ recordDaemonActivity(record: Omit<DaemonActivityRecord, 'timestamp'>): void;
130
+ /** Register (or clear, with null) the daemon manager's live-state source. */
131
+ setDaemonsProvider(provider: (() => DaemonStateSnapshot[]) | null): void;
90
132
  recordAuth(record: Omit<FunctionAuthRecord, 'timestamp'>): void;
91
133
  snapshot(): {
92
134
  counters: {
@@ -108,12 +150,18 @@ export declare const functionsStatusTracker: {
108
150
  total: number;
109
151
  denied: number;
110
152
  };
153
+ daemons: {
154
+ invocations: number;
155
+ failed: number;
156
+ };
111
157
  };
158
+ daemons: DaemonStateSnapshot[];
112
159
  history: {
113
160
  calls: FunctionCallRecord[];
114
161
  uploads: FunctionUploadRecord[];
115
162
  downloads: FunctionDownloadRecord[];
116
163
  auth: FunctionAuthRecord[];
164
+ daemons: DaemonActivityRecord[];
117
165
  };
118
166
  };
119
167
  };