@fugood/buttress-server 2.26.0-beta.16 → 2.26.0-beta.18

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 (41) hide show
  1. package/README.md +185 -4
  2. package/config/function-samples/README.md +1 -0
  3. package/config/function-samples/classify-ticket.ts +73 -0
  4. package/config/sample.toml +39 -0
  5. package/lib/agent/codemode-worker.d.ts +1 -0
  6. package/lib/agent/codemode.d.ts +7 -0
  7. package/lib/agent/decisions.check.d.ts +1 -0
  8. package/lib/agent/decisions.d.ts +113 -0
  9. package/lib/agent/mcp.d.ts +2 -3
  10. package/lib/agent/models.d.ts +4 -2
  11. package/lib/agent/sessions.d.ts +21 -8
  12. package/lib/agent/types.d.ts +30 -0
  13. package/lib/{cli-CtBbHMYQ.mjs → cli-CIa2rDj7.mjs} +2 -2
  14. package/lib/{client-DzfRSFcJ.mjs → client-BVyIuSyr.mjs} +1 -1
  15. package/lib/codemode-BxD8KKWZ.mjs +112 -0
  16. package/lib/codemode-worker.mjs +3 -0
  17. package/lib/config-cxApm_PZ.mjs +2 -0
  18. package/lib/functions/templates.d.ts +1 -1
  19. package/lib/functions/types.d.ts +7 -1
  20. package/lib/index.d.ts +4 -2
  21. package/lib/index.mjs +148 -68
  22. package/lib/mcp-Cy5Jhrn5.mjs +5 -0
  23. package/lib/protocol-RJSV46H1.mjs +351 -0
  24. package/lib/quickjs.wasm +0 -0
  25. package/lib/routes/decision-shared.d.ts +28 -0
  26. package/lib/routes/generator-cache.d.ts +3 -1
  27. package/lib/routes/index.d.ts +1 -0
  28. package/lib/routes/llm-shared.d.ts +1 -0
  29. package/lib/routes/typesafe-compat.check.d.ts +1 -0
  30. package/lib/routes/typesafe-compat.d.ts +31 -0
  31. package/lib/services/ggml-decision.d.ts +80 -0
  32. package/lib/services/ggml-llm.d.ts +29 -1
  33. package/lib/services/index.d.ts +3 -0
  34. package/lib/{tui-DbQ0zW-C.mjs → tui-Bxeb1d7E.mjs} +1 -1
  35. package/lib/types.d.ts +25 -1
  36. package/lib/utils/cors.d.ts +1 -0
  37. package/lib/utils/decision-sharing.d.ts +37 -0
  38. package/package.json +10 -9
  39. package/public/status.html +104 -1
  40. package/lib/agent/session-fs.d.ts +0 -3
  41. package/lib/config-DbRjQnNp.mjs +0 -2
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import type { ReadableStream } from 'node:stream/web';
3
+ import type { DecisionResult, ServiceContext, EventStream, Expand } from '../types';
4
+ export declare const decideProperty: z.ZodObject<{
5
+ request: z.ZodObject<{
6
+ state: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodUnknown, "many">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
7
+ questions: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
8
+ images: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ state: string | unknown[] | Record<string, unknown>;
11
+ questions: Record<string, Record<string, unknown>>;
12
+ images?: string[] | undefined;
13
+ }, {
14
+ state: string | unknown[] | Record<string, unknown>;
15
+ questions: Record<string, Record<string, unknown>>;
16
+ images?: string[] | undefined;
17
+ }>;
18
+ }, "strip", z.ZodTypeAny, {
19
+ request: {
20
+ state: string | unknown[] | Record<string, unknown>;
21
+ questions: Record<string, Record<string, unknown>>;
22
+ images?: string[] | undefined;
23
+ };
24
+ }, {
25
+ request: {
26
+ state: string | unknown[] | Record<string, unknown>;
27
+ questions: Record<string, Record<string, unknown>>;
28
+ images?: string[] | undefined;
29
+ };
30
+ }>;
31
+ export declare const schemas: {
32
+ initContext: z.ZodTuple<[z.ZodString, z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>], null>;
33
+ decide: z.ZodTuple<[z.ZodString, z.ZodObject<{
34
+ request: z.ZodObject<{
35
+ state: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodUnknown, "many">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
36
+ questions: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
37
+ images: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
38
+ }, "strip", z.ZodTypeAny, {
39
+ state: string | unknown[] | Record<string, unknown>;
40
+ questions: Record<string, Record<string, unknown>>;
41
+ images?: string[] | undefined;
42
+ }, {
43
+ state: string | unknown[] | Record<string, unknown>;
44
+ questions: Record<string, Record<string, unknown>>;
45
+ images?: string[] | undefined;
46
+ }>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ request: {
49
+ state: string | unknown[] | Record<string, unknown>;
50
+ questions: Record<string, Record<string, unknown>>;
51
+ images?: string[] | undefined;
52
+ };
53
+ }, {
54
+ request: {
55
+ state: string | unknown[] | Record<string, unknown>;
56
+ questions: Record<string, Record<string, unknown>>;
57
+ images?: string[] | undefined;
58
+ };
59
+ }>], null>;
60
+ releaseContext: z.ZodTuple<[z.ZodString], null>;
61
+ };
62
+ export interface Service {
63
+ initContext: (ctx: ServiceContext, ...args: z.infer<typeof schemas.initContext>) => ReadableStream<Expand<EventStream>>;
64
+ decide: (ctx: ServiceContext, ...args: z.infer<typeof schemas.decide>) => Promise<DecisionResult>;
65
+ releaseContext: (ctx: ServiceContext, ...args: z.infer<typeof schemas.releaseContext>) => Promise<{
66
+ released: boolean;
67
+ alreadyReleased?: boolean;
68
+ }>;
69
+ }
70
+ /**
71
+ * Clients upload local images first (like STT audio) and send their names;
72
+ * data URLs pass through.
73
+ */
74
+ export declare const resolveDecisionImages: (session: ServiceContext['session'], request: z.infer<typeof decideProperty>['request']) => {
75
+ state: string | unknown[] | Record<string, unknown>;
76
+ questions: Record<string, Record<string, unknown>>;
77
+ images: string[] | undefined;
78
+ };
79
+ declare const _default: Service;
80
+ export default _default;
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { type Service as LlmService } from './create-llm-service';
3
- import type { ServiceContext } from '../types';
3
+ import type { DecisionResult, ServiceContext } from '../types';
4
4
  export declare const schemas: {
5
5
  initContext: z.ZodTuple<[z.ZodString, z.ZodOptional<z.ZodAny>], null>;
6
6
  completion: z.ZodTuple<[z.ZodString, z.ZodOptional<z.ZodAny>], null>;
@@ -9,9 +9,37 @@ export declare const schemas: {
9
9
  applyChatTemplate: z.ZodTuple<[z.ZodString, z.ZodAny], null>;
10
10
  releaseContext: z.ZodTuple<[z.ZodString], null>;
11
11
  embedding: z.ZodTuple<[z.ZodString, z.ZodAny], null>;
12
+ decide: z.ZodTuple<[z.ZodString, z.ZodObject<{
13
+ request: z.ZodObject<{
14
+ state: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodUnknown, "many">, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
15
+ questions: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
16
+ images: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
17
+ }, "strip", z.ZodTypeAny, {
18
+ state: string | unknown[] | Record<string, unknown>;
19
+ questions: Record<string, Record<string, unknown>>;
20
+ images?: string[] | undefined;
21
+ }, {
22
+ state: string | unknown[] | Record<string, unknown>;
23
+ questions: Record<string, Record<string, unknown>>;
24
+ images?: string[] | undefined;
25
+ }>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ request: {
28
+ state: string | unknown[] | Record<string, unknown>;
29
+ questions: Record<string, Record<string, unknown>>;
30
+ images?: string[] | undefined;
31
+ };
32
+ }, {
33
+ request: {
34
+ state: string | unknown[] | Record<string, unknown>;
35
+ questions: Record<string, Record<string, unknown>>;
36
+ images?: string[] | undefined;
37
+ };
38
+ }>], null>;
12
39
  };
13
40
  export interface Service extends LlmService {
14
41
  embedding: (ctx: ServiceContext, ...args: z.infer<typeof schemas.embedding>) => Promise<any>;
42
+ decide: (ctx: ServiceContext, ...args: z.infer<typeof schemas.decide>) => Promise<DecisionResult>;
15
43
  }
16
44
  declare const _default: Service;
17
45
  export default _default;
@@ -3,6 +3,7 @@ import type { Service as CommonService } from './common';
3
3
  import type { Service as GgmlLlmService } from './ggml-llm';
4
4
  import type { Service as GgmlSttService } from './ggml-stt';
5
5
  import type { Service as GgmlTtsService } from './ggml-tts';
6
+ import type { Service as GgmlDecisionService } from './ggml-decision';
6
7
  import type { Service as MlxLlmService } from './mlx-llm';
7
8
  import type { Service as OnnxSttService } from './onnx-stt';
8
9
  import type { Service as OnnxTtsService } from './onnx-tts';
@@ -21,6 +22,7 @@ declare const services: {
21
22
  releaseContext: ({ backend, session }: ServiceContext, id: any, force: any) => Promise<any>;
22
23
  };
23
24
  ggmlTts: GgmlTtsService;
25
+ ggmlDecision: GgmlDecisionService;
24
26
  mlxLlm: MlxLlmService;
25
27
  onnxStt: OnnxSttService;
26
28
  onnxTts: OnnxTtsService;
@@ -35,6 +37,7 @@ export type Services = {
35
37
  ggmlLlm: ClientService<GgmlLlmService>;
36
38
  ggmlStt: ClientService<GgmlSttService>;
37
39
  ggmlTts: ClientService<GgmlTtsService>;
40
+ ggmlDecision: ClientService<GgmlDecisionService>;
38
41
  mlxLlm: ClientService<MlxLlmService>;
39
42
  onnxStt: ClientService<OnnxSttService>;
40
43
  onnxTts: ClientService<OnnxTtsService>;
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,t}from"./client-DzfRSFcJ.mjs";import{Container as n,Editor as r,Loader as i,Markdown as a,ProcessTerminal as o,Text as s,TuiMainScreen as c}from"@earendil-works/pi-tui";const l=e=>t=>`\x1b[${e}m${t}\x1b[0m`,u=l(`2`),d=l(`1`),f=l(`36`),p=l(`33`),m=l(`31`),h=l(`32`),g=l(`35`),_={heading:e=>d(f(e)),link:f,linkUrl:e=>u(f(e)),code:p,codeBlock:p,codeBlockBorder:u,quote:u,quoteBorder:u,hr:u,listBullet:f,bold:d,italic:l(`3`),strikethrough:l(`9`),underline:l(`4`)},v={borderColor:u,selectList:{selectedPrefix:f,selectedText:d,description:u,scrollInfo:u,noMatch:u}},y=(e,t)=>e.length>t?`${e.slice(0,t)}…`:e;var b=class{tui;chat;thinking=null;thinkingBuffer=``;answer=null;answerBuffer=``;constructor(e,t){this.tui=e,this.chat=t}touch(){this.tui.requestRender()}appendThinking(e){this.thinking||(this.thinking=new s(``,0,0),this.chat.addChild(this.thinking)),this.thinkingBuffer+=e,this.thinking.setText(u(this.thinkingBuffer.trimEnd())),this.touch()}appendAnswer(e){this.answer||(this.answer=new a(``,0,0,_),this.chat.addChild(this.answer)),this.answerBuffer+=e,this.answer.setText(this.answerBuffer),this.touch()}addLine(e){this.answer=null,this.answerBuffer=``,this.chat.addChild(new s(e,0,0)),this.touch()}};const x=async({connection:a,agentName:l,sessionId:_,fork:x})=>{let S=new o,C=new c(S);S.setTitle(`buttress agent · ${l}`);let w=new n,T=new s(``,0,0),E=new r(C,v);C.addChild(w),C.addChild(T),C.addChild(E),C.setFocus(E);let D=_,O=x,k=null,A=null,j=e=>{T.setText(e),C.requestRender()},M=()=>{A||(A=new i(C,f,u,`thinking…`),w.addChild(A),C.requestRender())},N=()=>{A&&(w.removeChild(A),A=null,C.requestRender())},P=`${d(l)} ${u(`@ ${a.baseUrl}`)}`+(D?` ${u(`(${O?`forking`:`continuing`} ${D})`)}`:``);w.addChild(new s(P,0,0)),j(u(`Enter to send · /new fresh session · /exit or Ctrl+C to quit`));let F=async()=>{C.stop(),await S.drainInput().catch(()=>{}),process.exit(0)};C.addInputListener(e=>{if(e===``)return k?(k.abort(),k=null,{consume:!0}):(F(),{consume:!0})});let I=async n=>{w.addChild(new s(`${f(`>`)} ${n}`,0,0));let r=new b(C,w),i=D;k=new AbortController,M(),j(u(`running · Ctrl+C aborts this turn`));let o;try{o=await e(a,l,{prompt:n,sessionId:D??void 0,fork:O},{signal:k.signal,onFrame:({event:e,payload:t})=>{if(e===`session`&&t?.sessionId){t.sessionId!==D&&(D=t.sessionId,w.addChild(new s(u(`session ${D}`),0,0)));return}if(e!==`agent`)return;N();let n=t?.event;if(t?.type===`message_update`&&n)n.type===`thinking_delta`?r.appendThinking(n.delta??``):n.type===`text_delta`&&r.appendAnswer(n.delta??``);else if(t?.type===`tool_execution_start`){let e=JSON.stringify(t.args??{});r.addLine(`${p(`⚙ ${t.toolName}`)}${u(`(${y(e,120)})`)}`)}else t?.type===`tool_execution_end`?r.addLine(t.isError?m(`✗ ${t.toolName} failed`):u(`✓ ${t.toolName}`)):t?.type===`tool_emit`&&r.addLine(u(` ${t.toolName} → ${t.event}`))}})}catch(e){o={kind:`error`,message:e?.message||String(e),sessionId:D}}if(N(),k=null,o.kind===`result`){let{result:e}=o;D=e.sessionId??D;let t=e.usage||{};w.addChild(new s(u(`— ${e.stopReason} · ${t.totalTurns??`?`} turn(s) · ${t.input??0} in / ${t.output??0} out · session ${D}`),0,0))}else o.kind===`aborted`?(D=o.sessionId??D,w.addChild(new s(p(`(aborted — session is continuable)`),0,0))):o.kind===`error`?(D=o.sessionId??D,w.addChild(new s(m(`Run failed: ${o.message}`),0,0))):w.addChild(new s(m(`Connection lost mid-run.`),0,0));t(i,D)&&(O=!1),j(u(`Enter to send · /new fresh session · /exit or Ctrl+C to quit`)),C.requestRender()};E.onSubmit=e=>{let t=e.trim();if(E.setText(``),t){if(k){j(p(`A turn is still running — Ctrl+C aborts it.`));return}if(t===`/exit`||t===`/quit`){F();return}if(t===`/new`){D=null,O=!1,w.addChild(new s(`${h(`•`)} ${u(`started a fresh session`)}`,0,0)),C.requestRender();return}if(t===`/session`){w.addChild(new s(u(D?`session ${D}`:`no session yet`),0,0)),C.requestRender();return}I(t)}},w.addChild(new s(u(g(`ready`)),0,0)),C.start(),await new Promise(()=>{})};export{x as runAgentTui};
2
+ import{a as e,t}from"./client-BVyIuSyr.mjs";import{Container as n,Editor as r,Loader as i,Markdown as a,ProcessTerminal as o,Text as s,TuiMainScreen as c}from"@earendil-works/pi-tui";const l=e=>t=>`\x1b[${e}m${t}\x1b[0m`,u=l(`2`),d=l(`1`),f=l(`36`),p=l(`33`),m=l(`31`),h=l(`32`),g=l(`35`),_={heading:e=>d(f(e)),link:f,linkUrl:e=>u(f(e)),code:p,codeBlock:p,codeBlockBorder:u,quote:u,quoteBorder:u,hr:u,listBullet:f,bold:d,italic:l(`3`),strikethrough:l(`9`),underline:l(`4`)},v={borderColor:u,selectList:{selectedPrefix:f,selectedText:d,description:u,scrollInfo:u,noMatch:u}},y=(e,t)=>e.length>t?`${e.slice(0,t)}…`:e;var b=class{tui;chat;thinking=null;thinkingBuffer=``;answer=null;answerBuffer=``;constructor(e,t){this.tui=e,this.chat=t}touch(){this.tui.requestRender()}appendThinking(e){this.thinking||(this.thinking=new s(``,0,0),this.chat.addChild(this.thinking)),this.thinkingBuffer+=e,this.thinking.setText(u(this.thinkingBuffer.trimEnd())),this.touch()}appendAnswer(e){this.answer||(this.answer=new a(``,0,0,_),this.chat.addChild(this.answer)),this.answerBuffer+=e,this.answer.setText(this.answerBuffer),this.touch()}addLine(e){this.answer=null,this.answerBuffer=``,this.chat.addChild(new s(e,0,0)),this.touch()}};const x=async({connection:a,agentName:l,sessionId:_,fork:x})=>{let S=new o,C=new c(S);S.setTitle(`buttress agent · ${l}`);let w=new n,T=new s(``,0,0),E=new r(C,v);C.addChild(w),C.addChild(T),C.addChild(E),C.setFocus(E);let D=_,O=x,k=null,A=null,j=e=>{T.setText(e),C.requestRender()},M=()=>{A||(A=new i(C,f,u,`thinking…`),w.addChild(A),C.requestRender())},N=()=>{A&&(w.removeChild(A),A=null,C.requestRender())},P=`${d(l)} ${u(`@ ${a.baseUrl}`)}`+(D?` ${u(`(${O?`forking`:`continuing`} ${D})`)}`:``);w.addChild(new s(P,0,0)),j(u(`Enter to send · /new fresh session · /exit or Ctrl+C to quit`));let F=async()=>{C.stop(),await S.drainInput().catch(()=>{}),process.exit(0)};C.addInputListener(e=>{if(e===``)return k?(k.abort(),k=null,{consume:!0}):(F(),{consume:!0})});let I=async n=>{w.addChild(new s(`${f(`>`)} ${n}`,0,0));let r=new b(C,w),i=D;k=new AbortController,M(),j(u(`running · Ctrl+C aborts this turn`));let o;try{o=await e(a,l,{prompt:n,sessionId:D??void 0,fork:O},{signal:k.signal,onFrame:({event:e,payload:t})=>{if(e===`session`&&t?.sessionId){t.sessionId!==D&&(D=t.sessionId,w.addChild(new s(u(`session ${D}`),0,0)));return}if(e!==`agent`)return;N();let n=t?.event;if(t?.type===`message_update`&&n)n.type===`thinking_delta`?r.appendThinking(n.delta??``):n.type===`text_delta`&&r.appendAnswer(n.delta??``);else if(t?.type===`tool_execution_start`){let e=JSON.stringify(t.args??{});r.addLine(`${p(`⚙ ${t.toolName}`)}${u(`(${y(e,120)})`)}`)}else t?.type===`tool_execution_end`?r.addLine(t.isError?m(`✗ ${t.toolName} failed`):u(`✓ ${t.toolName}`)):t?.type===`tool_emit`&&r.addLine(u(` ${t.toolName} → ${t.event}`))}})}catch(e){o={kind:`error`,message:e?.message||String(e),sessionId:D}}if(N(),k=null,o.kind===`result`){let{result:e}=o;D=e.sessionId??D;let t=e.usage||{};w.addChild(new s(u(`— ${e.stopReason} · ${t.totalTurns??`?`} turn(s) · ${t.input??0} in / ${t.output??0} out · session ${D}`),0,0))}else o.kind===`aborted`?(D=o.sessionId??D,w.addChild(new s(p(`(aborted — session is continuable)`),0,0))):o.kind===`error`?(D=o.sessionId??D,w.addChild(new s(m(`Run failed: ${o.message}`),0,0))):w.addChild(new s(m(`Connection lost mid-run.`),0,0));t(i,D)&&(O=!1),j(u(`Enter to send · /new fresh session · /exit or Ctrl+C to quit`)),C.requestRender()};E.onSubmit=e=>{let t=e.trim();if(E.setText(``),t){if(k){j(p(`A turn is still running — Ctrl+C aborts it.`));return}if(t===`/exit`||t===`/quit`){F();return}if(t===`/new`){D=null,O=!1,w.addChild(new s(`${h(`•`)} ${u(`started a fresh session`)}`,0,0)),C.requestRender();return}if(t===`/session`){w.addChild(new s(u(D?`session ${D}`:`no session yet`),0,0)),C.requestRender();return}I(t)}},w.addChild(new s(u(g(`ready`)),0,0)),C.start(),await new Promise(()=>{})};export{x as runAgentTui};
package/lib/types.d.ts CHANGED
@@ -26,7 +26,25 @@ export type RuntimeConfig = {
26
26
  huggingface_token?: string;
27
27
  session_cache?: SessionCacheConfig;
28
28
  } & Record<string, any>;
29
- export type GeneratorType = 'ggml-llm' | 'ggml-stt' | 'ggml-tts' | 'mlx-llm' | 'onnx-stt' | 'onnx-tts';
29
+ export type GeneratorType = 'ggml-llm' | 'ggml-stt' | 'ggml-tts' | 'ggml-decision' | 'mlx-llm' | 'onnx-stt' | 'onnx-tts';
30
+ /** A TypeSafe `/v1/systemone` request, answered by a `ggml-decision` generator. */
31
+ export type DecisionRequest = {
32
+ /** A string, or any JSON object/array (given to the model as JSON text). */
33
+ state: unknown;
34
+ /** Question id → `{ type: 'choice' | 'score' | 'noul', instructions, criteria }`. */
35
+ questions: Record<string, Record<string, any>>;
36
+ /** Local file paths or data URLs; needs a model with an mmproj configured. */
37
+ images?: string[];
38
+ };
39
+ export type DecisionResult = {
40
+ model: string;
41
+ /** Question id → the answer for its type (`choice`, `score` or `noul`). */
42
+ answers: Record<string, any>;
43
+ usage: {
44
+ input_tokens: number;
45
+ output_tokens: number;
46
+ };
47
+ };
30
48
  export type GeneratorConfig = {
31
49
  type: GeneratorType;
32
50
  } & Record<string, any>;
@@ -40,6 +58,12 @@ export type GlobalConfig = {
40
58
  enabled?: boolean;
41
59
  cors_allowed_origins?: string | string[];
42
60
  };
61
+ typesafe_compat?: {
62
+ enabled?: boolean;
63
+ cors_allowed_origins?: string | string[];
64
+ /** Request `model` names (e.g. `jev-latest`) mapped to a decision generator id. */
65
+ models?: Record<string, string>;
66
+ };
43
67
  functions?: {
44
68
  enabled?: boolean;
45
69
  /** Directory holding local function files, relative to the config file. */
@@ -56,6 +56,7 @@ export type CorsPolicy = {
56
56
  export declare const buildCorsPolicies: (config: Config, enabled?: {
57
57
  openaiCompat?: boolean;
58
58
  anthropicMessages?: boolean;
59
+ typesafeCompat?: boolean;
59
60
  functions?: boolean;
60
61
  }) => CorsPolicy[];
61
62
  /**
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Decision models that share an LLM generator's context.
3
+ *
4
+ * A model that both generates and answers typed decisions (Jevling, for one)
5
+ * is often configured twice: as a `ggml-llm` and as a `ggml-decision`. With
6
+ * `[generators.backend] share_llm_context = true` on the decision entry, and
7
+ * both entries naming the same weights, decisions run on the LLM's context
8
+ * (`parallel.decide()` on a slot beside its completions) instead of loading
9
+ * the weights a second time. The LLM config always loads the model, so the
10
+ * result does not depend on which generator starts first.
11
+ *
12
+ * It is opt-in because it trades throughput for memory: a decision takes one
13
+ * of the LLM's `n_parallel` slots, and its forward passes run in the slot
14
+ * loop between two shared batches, so concurrent completions stop generating
15
+ * while it runs (a 128-token completion took 2x as long beside six decisions,
16
+ * unchanged with a separate decision context). Results are not affected: the
17
+ * slot's sequence and prompt cache are cleared after each decision.
18
+ *
19
+ * Sharing is skipped when the decision needs a context the LLM cannot give:
20
+ * rank pooling (an embedding context), or a different projector.
21
+ */
22
+ /**
23
+ * The configured `ggml-llm` generator whose context serves this configured
24
+ * decision generator, or null. For a client's request use
25
+ * `findSharedLlmForClient`: the opt-in must come from the server config.
26
+ */
27
+ export declare const findSharedLlmGenerator: (config: any, decisionConfig: any) => any | null;
28
+ /**
29
+ * The shared LLM for a decision generator a client starts over RPC, or null.
30
+ * Sharing needs a configured `ggml-decision` entry with the same weights that
31
+ * opted in; the client's own `share_llm_context` is ignored, since it would
32
+ * let a client put its decisions on the LLM's slots. The client's merged
33
+ * model (rank pooling, projector) still decides whether the LLM can serve it.
34
+ */
35
+ export declare const findSharedLlmForClient: (config: any, clientModel: any) => any | null;
36
+ /** Startup report: which decision generators share an LLM, and why others do not. */
37
+ export declare const describeDecisionSharing: (config: any) => string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fugood/buttress-server",
3
- "version": "2.26.0-beta.16",
3
+ "version": "2.26.0-beta.18",
4
4
  "main": "lib/index.mjs",
5
5
  "types": "lib/index.d.ts",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "typecheck": "tsc --noEmit",
22
- "build": "tsdown -c rolldown.config.js --config-loader native && tsc --noCheck --emitDeclarationOnly && cp ../buttress-backend-core/src/backends/mlx-bridge.py lib/mlx-bridge.py",
22
+ "build": "tsdown -c rolldown.config.js --config-loader native && tsc --noCheck --emitDeclarationOnly && cp ../buttress-backend-core/src/backends/mlx-bridge.py lib/mlx-bridge.py && cp ../../node_modules/quickjs-wasi/quickjs.wasm lib/quickjs.wasm",
23
23
  "build:dist": "$npm_execpath scripts/build-distribution.js",
24
24
  "release": "$npm_execpath scripts/build-distribution.js",
25
25
  "release-beta": "$npm_execpath scripts/build-distribution.js --beta",
@@ -37,12 +37,12 @@
37
37
  ],
38
38
  "license": "MIT",
39
39
  "dependencies": {
40
- "@earendil-works/pi-agent-core": "^0.99.1",
41
- "@earendil-works/pi-ai": "0.99.1",
42
- "@earendil-works/pi-tui": "^0.99.1",
40
+ "@earendil-works/pi-agent-core": "^1.0.0",
41
+ "@earendil-works/pi-ai": "1.0.0",
42
+ "@earendil-works/pi-tui": "^1.0.0",
43
43
  "@elysiajs/cors": "^1.1.1",
44
44
  "@elysiajs/node": "^1.4.2",
45
- "@fugood/llama.node": "^1.8.0-rc.8",
45
+ "@fugood/llama.node": "^1.8.0-rc.9",
46
46
  "@fugood/whisper.node": "^1.2.0-rc.0",
47
47
  "@huggingface/gguf": "^0.3.2",
48
48
  "@iarna/toml": "^3.0.0",
@@ -69,10 +69,11 @@
69
69
  "zod": "^3.25.76"
70
70
  },
71
71
  "devDependencies": {
72
- "@fugood/bricks-cli": "^2.26.0-beta.16",
73
- "@fugood/bricks-devtools-core": "^2.26.0-beta.11",
72
+ "@fugood/agent-tool-runtime": "^2.26.0-beta.18",
73
+ "@fugood/bricks-cli": "^2.26.0-beta.18",
74
+ "@fugood/bricks-devtools-core": "^2.26.0-beta.18",
74
75
  "tsdown": "^0.22.4",
75
76
  "typescript": "^7.0.2"
76
77
  },
77
- "gitHead": "c9cc188d85392d229bb7b143f9bf56e0d6f1cb23"
78
+ "gitHead": "56a905825a9905fbcf48a3ee7dcc689759b7f1f4"
78
79
  }
@@ -603,6 +603,27 @@
603
603
  </div>
604
604
  </div>
605
605
 
606
+ <!-- GGML Typed Decision Status (hidden unless active) -->
607
+ <div class="card" id="ggmlDecisionCard" style="display:none">
608
+ <div class="card-header">
609
+ <span class="card-title">Typed Decision Status</span>
610
+ <span class="badge badge-info" id="ggmlDecisionGeneratorCount">0 generators</span>
611
+ </div>
612
+ <div id="ggmlDecisionGenerators"></div>
613
+ <div class="section">
614
+ <div class="section-title collapsible" onclick="toggleSection(this)">Model Load History</div>
615
+ <div class="collapsible-content" id="ggmlDecisionModelHistory">
616
+ <div class="empty-state">No model load history</div>
617
+ </div>
618
+ </div>
619
+ <div class="section">
620
+ <div class="section-title collapsible" onclick="toggleSection(this)">Decision History</div>
621
+ <div class="collapsible-content" id="ggmlDecisionHistory">
622
+ <div class="empty-state">No decision history</div>
623
+ </div>
624
+ </div>
625
+ </div>
626
+
606
627
  <!-- Local Functions (hidden unless enabled) -->
607
628
  <div class="card" id="functionsCard" style="display:none">
608
629
  <div class="card-header">
@@ -1049,6 +1070,54 @@
1049
1070
  })
1050
1071
  }
1051
1072
 
1073
+ function renderGgmlDecisionGenerators(generators) {
1074
+ const container = document.getElementById('ggmlDecisionGenerators')
1075
+ document.getElementById('ggmlDecisionGeneratorCount').textContent =
1076
+ `${generators.length} generator${generators.length !== 1 ? 's' : ''}`
1077
+
1078
+ withScrollPreserve(container, () => {
1079
+ if (generators.length === 0) {
1080
+ container.innerHTML = '<div class="empty-state">No typed decision generators loaded</div>'
1081
+ return
1082
+ }
1083
+
1084
+ container.innerHTML = `
1085
+ <div class="table-wrapper">
1086
+ <div class="table-inner">
1087
+ <table>
1088
+ <thead>
1089
+ <tr>
1090
+ <th>ID</th>
1091
+ <th>Model</th>
1092
+ <th>Type</th>
1093
+ <th>Quant</th>
1094
+ <th>Variant</th>
1095
+ <th>GPU</th>
1096
+ <th>Model Size</th>
1097
+ <th>Context</th>
1098
+ </tr>
1099
+ </thead>
1100
+ <tbody>
1101
+ ${generators.map(g => `
1102
+ <tr>
1103
+ <td class="mono">${escapeHtml(g.id?.slice(0, 8) || '-')}</td>
1104
+ <td>${formatModelName(g.repoId)}${g.mmprojFilename ? '<br><span style="font-size:11px;color:var(--text-muted)">+ ' + escapeHtml(g.mmprojFilename) + '</span>' : ''}</td>
1105
+ <td>${escapeHtml(g.decisionType || '-')}</td>
1106
+ <td>${escapeHtml(g.quantization || '-')}</td>
1107
+ <td>${escapeHtml(g.variant || '-')}</td>
1108
+ <td><span class="badge ${g.useGpu ? 'badge-success' : 'badge-info'}">${g.useGpu ? 'yes' : 'no'}</span></td>
1109
+ <td>${formatBytes(g.modelBytes)}</td>
1110
+ <td>${g.hasContext ? '<span class="badge badge-success">loaded</span>' : '-'}</td>
1111
+ </tr>
1112
+ `).join('')}
1113
+ </tbody>
1114
+ </table>
1115
+ </div>
1116
+ </div>
1117
+ `
1118
+ })
1119
+ }
1120
+
1052
1121
  // Escape HTML
1053
1122
  function escapeHtml(str) {
1054
1123
  if (str == null) return ''
@@ -1077,6 +1146,7 @@
1077
1146
  const onnxStt = status.onnxStt || {}
1078
1147
  const onnxTts = status.onnxTts || {}
1079
1148
  const ggmlTts = status.ggmlTts || {}
1149
+ const ggmlDecision = status.ggmlDecision || {}
1080
1150
 
1081
1151
  // LLM
1082
1152
  renderLlmGenerators(llm.generators || [])
@@ -1288,6 +1358,39 @@
1288
1358
  },
1289
1359
  ])
1290
1360
 
1361
+ // GGML typed decisions
1362
+ const hasGgmlDecision = (ggmlDecision.generators?.length > 0) ||
1363
+ (ggmlDecision.history?.modelLoads?.length > 0) ||
1364
+ (ggmlDecision.history?.decisions?.length > 0)
1365
+ document.getElementById('ggmlDecisionCard').style.display = hasGgmlDecision ? '' : 'none'
1366
+ renderGgmlDecisionGenerators(ggmlDecision.generators || [])
1367
+
1368
+ renderHistory('ggmlDecisionModelHistory', ggmlDecision.history?.modelLoads || [], [
1369
+ { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1370
+ { label: 'Model', render: i => formatModelName(i.repoId) },
1371
+ { label: 'Type', render: i => escapeHtml(i.decisionType || '-') },
1372
+ { label: 'Quant', render: i => escapeHtml(i.quantization || '-') },
1373
+ { label: 'Variant', render: i => escapeHtml(i.variant || '-') },
1374
+ { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(1)}s` },
1375
+ { label: 'Status', render: i => i.success ?
1376
+ '<span class="badge badge-success">Success</span>' :
1377
+ `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1378
+ },
1379
+ ])
1380
+
1381
+ renderHistory('ggmlDecisionHistory', ggmlDecision.history?.decisions || [], [
1382
+ { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1383
+ { label: 'Model', render: i => formatModelName(i.repoId) },
1384
+ { label: 'Questions', render: i => i.questionCount ?? '-' },
1385
+ { label: 'Images', render: i => i.imageCount || '-' },
1386
+ { label: 'Tokens', render: i => i.inputTokens ?? '-' },
1387
+ { label: 'Duration', render: i => `${i.durationMs} ms` },
1388
+ { label: 'Status', render: i => i.success ?
1389
+ '<span class="badge badge-success">Success</span>' :
1390
+ `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1391
+ },
1392
+ ])
1393
+
1291
1394
  renderFunctionsStatus(status.functions || {})
1292
1395
  }
1293
1396
 
@@ -1447,7 +1550,7 @@
1447
1550
  const response = await fetch('/buttress/status')
1448
1551
  const data = await response.json()
1449
1552
  // Direct JSON response (not tRPC wrapped)
1450
- if (data.ggmlLlm || data.ggmlStt || data.mlxLlm || data.onnxStt || data.onnxTts || data.ggmlTts) {
1553
+ if (data.ggmlLlm || data.ggmlStt || data.mlxLlm || data.onnxStt || data.onnxTts || data.ggmlTts || data.ggmlDecision) {
1451
1554
  updateUI(data)
1452
1555
  setConnectionStatus('connected')
1453
1556
  }
@@ -1,3 +0,0 @@
1
- import type { FileSystem } from '@earendil-works/pi-agent-core';
2
- /** File storage for agent sessions that classifies errors structurally across vm realms. */
3
- export declare const createSessionFs: (rootDir: string) => FileSystem;
@@ -1,2 +0,0 @@
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)}}if(e?.local_devices!==void 0&&typeof e.local_devices!=`boolean`)throw new y(`agent "${r}": local_devices must be a boolean`);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,local_devices:g=!1,mcp_servers:v,max_turns:b,max_tokens_per_run:S,thinking:C,...w}=e;return{name:r,provider:o,modelId:s,modelRef:i,systemPrompt:typeof p==`string`?p:null,systemPromptFile:typeof m==`string`?m:null,tools:c,localDevices:g,mcpServers:x(r,v),maxTurns:l,maxTokensPerRun:u,thinking:typeof C==`string`?C:void 0,generation:w}},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};