@gobing-ai/ts-ai-runner 0.5.2 → 0.5.3

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
@@ -34,7 +34,7 @@ bun add @gobing-ai/ts-ai-runner
34
34
  | `isAgentName()` | Type guard for supported agent identifiers |
35
35
  | `createDecisionMaker()` / `q` | Batch `ask` and single-question `choice`/`score`/`noul` decisions with provider-neutral question/answer types and a `DecisionError` taxonomy |
36
36
 
37
- Supported agent identifiers: `claude`, `codex`, `gemini` (deprecated), `pi`, `omp`, `opencode`, `antigravity-cli`, `openclaw`, `hermes`, `grok`, `deepseek`. The `antigravity` id is a deprecated alias of `antigravity-cli`. See [Deprecation & Aliases](#deprecation--aliases).
37
+ Supported agent identifiers: `claude`, `codex`, `gemini` (deprecated), `pi`, `omp`, `opencode`, `antigravity-cli`, `openclaw`, `hermes`, `grok`, `deepseek`, `fm` (text-only). The `antigravity` id is a deprecated alias of `antigravity-cli`. See [Deprecation & Aliases](#deprecation--aliases).
38
38
 
39
39
  ## Architecture
40
40
 
@@ -347,6 +347,7 @@ const { args } = getAgentShim('omp').getPromptCommand({
347
347
  | openclaw | ✗ (CLI `--session-id` unwired) | ✗ | degrade → fresh `agent` | ✗ | ✓ (CLI `--json`; unwired) |
348
348
  | hermes | ✗ (unverified) | ✗ | degrade → fresh `chat` | ✗ | ✗ (unverified) |
349
349
  | deepseek | ✗ | ✗ | degrade → fresh headless one-shot | ✗ | ✗ |
350
+ | fm | ✓ | ✓ | `--resume <file>` + `--save-transcript <file>` (file = `<sessionDir>/<sessionId>.json`) | ✗ (`fm chat` is interactive only) | ✗ (`--schema` needs a file; ts-decision-fm surface) |
350
351
 
351
352
  **Claude session discovery:** claude has no session-dir flag and does not accept a caller-chosen
352
353
  session id at open; the headless run's session id is discovered from the process output after the
@@ -505,6 +506,7 @@ Agent-specific behavior:
505
506
  | `hermes` | `hermes` | 1 | `hermes doctor` | `chat -q`, `--continue`, `-m` |
506
507
  | `grok` | `grok` | 1 | env/file (`XAI_API_KEY` or `~/.grok/auth.json`) | `-p`, `-c` (resume), `-m`, `--output-format plain\|json` (maps ai-runner `text` → `plain`) |
507
508
  | `deepseek` | `dsh` | 1 | env-only / `~/.dsh` (no status verb) | `--profile headless <task>`; session/model options degrade to a fresh one-shot (no flags at 0.1.5-rc.1) |
509
+ | `fm` | `fm` | 1 | `fm available --model system` | `respond --no-stream`, `-m`, transcript session flags; **text-only** (no file/shell tools — excluded from `TIER1_PRIORITY` auto-selection) |
508
510
 
509
511
  This is the right layer for UI previews, audit logging, and custom launchers.
510
512
 
@@ -738,6 +740,12 @@ shared state and get typed, discriminated answers back. Questions are built with
738
740
  answers are decoded into neutral `ChoiceAnswer` / `ScoreAnswer` / `NoulAnswer` types — no SDK type
739
741
  crosses this boundary.
740
742
 
743
+ Named backends are selected with `backend:`: `typesafe` (the default, reads `TYPESAFE_API_KEY`),
744
+ `laya-local` (`@gobing-ai/ts-laya-mlx`), and `fm-local` (`@gobing-ai/ts-decision-fm`). Both local
745
+ backends resolve their driver by dynamic import on first use — this package declares no dependency
746
+ on either driver package (ADR-028); install the one you select. For typed, non-default driver
747
+ options, construct the driver yourself and pass it as `driver:`.
748
+
741
749
  ### Batch `ask` — many questions, one request
742
750
 
743
751
  All questions in one `ask` call share the state and cost a single backend request:
@@ -1 +1 @@
1
- {"version":3,"file":"auth-shims.d.ts","sourceRoot":"","sources":["../../src/agents/auth-shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAA2B,MAAM,uBAAuB,CAAC;AACjF,OAAO,KAAK,EAAmC,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,KAAK,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,iBAAiB,GAAG,SAAS,CAAC;AAExE,qDAAqD;AACrD,MAAM,WAAW,WAAW;IACxB,0CAA0C;IAC1C,MAAM,EAAE,QAAQ,CAAC;IACjB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,0EAA0E;IAC1E,UAAU,EAAE,UAAU,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AA6CD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAiB5F;AAkFD,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"auth-shims.d.ts","sourceRoot":"","sources":["../../src/agents/auth-shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAA2B,MAAM,uBAAuB,CAAC;AACjF,OAAO,KAAK,EAAmC,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,KAAK,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,iBAAiB,GAAG,SAAS,CAAC;AAExE,qDAAqD;AACrD,MAAM,WAAW,WAAW;IACxB,0CAA0C;IAC1C,MAAM,EAAE,QAAQ,CAAC;IACjB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,0EAA0E;IAC1E,UAAU,EAAE,UAAU,CAAC;IACvB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAqDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAiB5F;AAkFD,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -34,6 +34,14 @@ const AUTH_PATTERNS = {
34
34
  positive: /(^|[^a-z])ok([^a-z]|$)|healthy|configured|ready/i,
35
35
  negative: /not[\s_-]*(configured|healthy|ok)|unhealthy|missing[\s_-]+dependenc|error|failed/i,
36
36
  },
37
+ // fm reports model availability, not login state: doctor "authenticated"
38
+ // means "system model available". The negative test runs first in
39
+ // probeAuthOutput, so `System model unavailable: <reason>` (exit 1) never
40
+ // false-matches the positive phrase.
41
+ fm: {
42
+ positive: /System model available/i,
43
+ negative: /unavailable/i,
44
+ },
37
45
  };
38
46
  /** True when a value is a defined, non-blank string. */
39
47
  function isNonEmpty(value) {
@@ -1,5 +1,5 @@
1
1
  /** Identifier for one supported coding agent (canonical id). */
2
- export type AgentName = 'claude' | 'codex' | 'gemini' | 'pi' | 'opencode' | 'antigravity-cli' | 'openclaw' | 'hermes' | 'omp' | 'grok' | 'deepseek';
2
+ export type AgentName = 'claude' | 'codex' | 'gemini' | 'pi' | 'opencode' | 'antigravity-cli' | 'openclaw' | 'hermes' | 'omp' | 'grok' | 'deepseek' | 'fm';
3
3
  /** Output mode for prompt invocations. */
4
4
  export type OutputMode = 'text' | 'json';
5
5
  /** Concrete executable and argv pair returned by an agent shim. */
@@ -88,6 +88,8 @@ export interface AgentShim {
88
88
  readonly aliases?: readonly string[];
89
89
  /** Deprecation marker; resolving a deprecated id warns and reports canonical status. */
90
90
  readonly deprecated?: AgentDeprecation;
91
+ /** Prompt-only LLM: no file-writing or shell tools. Callers dispatching tool-using work must refuse it. */
92
+ readonly textOnly?: boolean;
91
93
  /** Build a help-display command. */
92
94
  getHelpCommand(): ShimCommand;
93
95
  /** Build a version-detection command. */
@@ -1 +1 @@
1
- {"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/agents/shims.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/agents/shims.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { getLogger } from '@gobing-ai/ts-infra';
2
+ import { joinPath } from '@gobing-ai/ts-runtime';
2
3
  /** pi-family rpc dialect (pi docs/rpc.md): one JSON object per line. */
3
4
  const piRpcFrame = (input) => `${JSON.stringify({ type: 'prompt', message: input })}\n`;
4
5
  /** claude stream-json input dialect: one user-message JSONL envelope per line. */
@@ -315,6 +316,56 @@ const dshShim = {
315
316
  },
316
317
  getAuthCommand: () => null,
317
318
  };
319
+ /**
320
+ * Apple Foundation Model CLI (`/usr/bin/fm`, macOS 27) — tier-1, text-only.
321
+ * A plain on-device LLM with no file or shell tools, so it must never be
322
+ * auto-selected for tool-using work (it is deliberately absent from
323
+ * `TIER1_PRIORITY`; the consumer is spur `agent-service.ts`).
324
+ *
325
+ * No `--version` flag (`fm --version` prints "Unknown option" and exits 0),
326
+ * so the version comes from the binary's embedded SCCS build string via
327
+ * `what -q /usr/bin/fm` — absolute path deliberate, `fm` ships only there;
328
+ * `what` exits 1 when the file is absent, so Linux/older macOS report
329
+ * not-installed through the existing non-zero-exit branch.
330
+ *
331
+ * Auth is the system-model availability probe; bare `fm available` always
332
+ * exits 0 and is never used. The transcript file is the session (ADR-047 R5):
333
+ * `--resume <file> --save-transcript <file>` resumes it, a missing transcript
334
+ * makes `fm` exit 1 ("Unable to read transcript at …") which surfaces as a
335
+ * failed run, never a silent fresh start. The shim stays pure — no
336
+ * filesystem I/O. Structured output (`--schema`) belongs to ts-decision-fm.
337
+ */
338
+ const fmShim = {
339
+ name: 'fm',
340
+ command: 'fm',
341
+ tier: 1,
342
+ textOnly: true,
343
+ getHelpCommand: () => ({ command: 'fm', args: ['--help'] }),
344
+ getVersionCommand: () => ({ command: 'what', args: ['-q', '/usr/bin/fm'] }),
345
+ getPromptCommand: (options) => {
346
+ // Headless one-shot text response. mode is ignored: `json` has no
347
+ // schema to pass through the generic PromptOptions (that surface is
348
+ // ts-decision-fm's `--schema` bridge).
349
+ const args = ['respond', '--no-stream'];
350
+ if (options.model !== undefined)
351
+ args.push('-m', options.model);
352
+ // ADR-047 R5: sessionId/sessionDir select the scoped transcript file
353
+ // and suppress any unscoped continue. `continue` alone degrades to a
354
+ // fresh call — fm has no implicit last-session flag.
355
+ if (options.sessionId !== undefined) {
356
+ const file = options.sessionDir !== undefined
357
+ ? joinPath(options.sessionDir, `${options.sessionId}.json`)
358
+ : `${options.sessionId}.json`;
359
+ args.push('--resume', file, '--save-transcript', file);
360
+ }
361
+ else if (options.sessionDir !== undefined) {
362
+ args.push('--save-transcript', joinPath(options.sessionDir, 'fm-session.json'));
363
+ }
364
+ args.push(options.input ?? '');
365
+ return { command: 'fm', args };
366
+ },
367
+ getAuthCommand: () => ({ command: 'fm', args: ['available', '--model', 'system'] }),
368
+ };
318
369
  /** All bundled agent shims keyed by canonical agent name. */
319
370
  export const AGENT_SHIMS = {
320
371
  claude: claudeShim,
@@ -328,6 +379,7 @@ export const AGENT_SHIMS = {
328
379
  omp: ompShim,
329
380
  grok: grokShim,
330
381
  deepseek: dshShim,
382
+ fm: fmShim,
331
383
  };
332
384
  /**
333
385
  * Session-affinity capability metadata per agent. Callers must consult this
@@ -438,6 +490,14 @@ const AGENT_SESSION_CAPABILITY = {
438
490
  verifiedAgainst: '0.1.5-rc.1',
439
491
  note: 'headless app takes only the task positional and -h at 0.1.5-rc.1 — no resume/session/mode flags; fresh-dispatch degrade',
440
492
  },
493
+ fm: {
494
+ supportsResumeById: true,
495
+ supportsSessionDir: true,
496
+ supportsPersistentStdin: false,
497
+ supportsStructuredOutput: false,
498
+ verifiedAgainst: 'FoundationModels-2.0.68.1.402',
499
+ note: '`fm chat` is interactive only — no multi-turn stdin input mode; structured output needs `--schema <file>`, which the generic PromptOptions cannot carry (ts-decision-fm surface)',
500
+ },
441
501
  };
442
502
  /** Query a bundled agent's session-affinity capability by canonical name. */
443
503
  export function getAgentSessionCapability(agent) {
@@ -468,6 +528,7 @@ export const DISPLAY_ORDER = [
468
528
  'hermes',
469
529
  'grok',
470
530
  'deepseek',
531
+ 'fm',
471
532
  ];
472
533
  /** Set of gateway/TUI-constrained agents. */
473
534
  export const TIER2_AGENTS = new Set(['openclaw']);
@@ -19,8 +19,8 @@ export interface DecisionMaker {
19
19
  no?: Desc;
20
20
  }): Promise<NoulAnswer>;
21
21
  }
22
- /** Available named decision backends (task 0080). */
23
- export type DecisionBackend = 'typesafe' | 'laya-local';
22
+ /** Available named decision backends (task 0080, task 0085). */
23
+ export type DecisionBackend = 'typesafe' | 'laya-local' | 'fm-local';
24
24
  /** Factory options: driver selection, credential injection, and transport tuning. */
25
25
  export interface DecisionMakerOptions {
26
26
  /** Custom driver — when supplied, `backend` is ignored, no default driver is constructed and no key is required. */
@@ -1 +1 @@
1
- {"version":3,"file":"decision-maker.d.ts","sourceRoot":"","sources":["../../src/decision/decision-maker.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACR,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,IAAI,EACJ,UAAU,EACV,QAAQ,EACR,WAAW,EACd,MAAM,SAAS,CAAC;AAKjB,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE;QAC/C,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,CAAC,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3B,yEAAyE;IACzE,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EACzB,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,IAAI,EACZ,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,GACxB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,gFAAgF;IAChF,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1G,wEAAwE;IACxE,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,IAAI,CAAC;QAAC,EAAE,CAAC,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACxG;AAED,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,YAAY,CAAC;AAExD,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACjC,oHAAoH;IACpH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,4GAA4G;IAC5G,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAsCD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAoDrF"}
1
+ {"version":3,"file":"decision-maker.d.ts","sourceRoot":"","sources":["../../src/decision/decision-maker.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACR,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,IAAI,EACJ,UAAU,EACV,QAAQ,EACR,WAAW,EACd,MAAM,SAAS,CAAC;AAKjB,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE;QAC/C,KAAK,EAAE,aAAa,CAAC;QACrB,SAAS,EAAE,CAAC,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3B,yEAAyE;IACzE,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EACzB,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,IAAI,EACZ,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,GACxB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,gFAAgF;IAChF,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1G,wEAAwE;IACxE,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,IAAI,CAAC;QAAC,EAAE,CAAC,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACxG;AAED,gEAAgE;AAChE,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC;AAErE,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACjC,oHAAoH;IACpH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,4GAA4G;IAC5G,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AA0DD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAwDrF"}
@@ -34,6 +34,19 @@ async function resolveLayaDriver(options) {
34
34
  throw new DecisionConfigError(`The 'laya-local' backend requires '${LAYA_DRIVER_PACKAGE}' to be installed; install it with 'bun add ${LAYA_DRIVER_PACKAGE}'`, 'LAYA_BACKEND', { cause });
35
35
  }
36
36
  }
37
+ const FM_DRIVER_PACKAGE = '@gobing-ai/ts-decision-fm';
38
+ async function resolveFmDriver(options) {
39
+ try {
40
+ const mod = (await import(FM_DRIVER_PACKAGE));
41
+ if (typeof mod.createFmDriver !== 'function') {
42
+ throw new Error(`Module '${FM_DRIVER_PACKAGE}' does not export createFmDriver`);
43
+ }
44
+ return mod.createFmDriver(options);
45
+ }
46
+ catch (cause) {
47
+ throw new DecisionConfigError(`The 'fm-local' backend requires '${FM_DRIVER_PACKAGE}' to be installed; install it with 'bun add ${FM_DRIVER_PACKAGE}'`, 'FM_BACKEND', { cause });
48
+ }
49
+ }
37
50
  /**
38
51
  * Build a DecisionMaker. `options.driver` wins when supplied; otherwise `options.backend`
39
52
  * resolves the driver (default `'typesafe'`). The driver is constructed lazily on first use.
@@ -61,6 +74,10 @@ export function createDecisionMaker(options = {}) {
61
74
  resolvedDriver = await resolveLayaDriver(options);
62
75
  return resolvedDriver;
63
76
  }
77
+ if (backend === 'fm-local') {
78
+ resolvedDriver = await resolveFmDriver(options);
79
+ return resolvedDriver;
80
+ }
64
81
  throw new DecisionConfigError(`Unknown decision backend '${backend}'`, 'backend');
65
82
  };
66
83
  const ask = async (req) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-ai-runner",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "@gobing-ai/ts-ai-runner — Coding-agent shims, detection, doctor checks, and prompt execution.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,13 +47,13 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-ai-runner-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-infra": "^0.5.2",
51
- "@gobing-ai/ts-runtime": "^0.5.2",
50
+ "@gobing-ai/ts-infra": "^0.5.3",
51
+ "@gobing-ai/ts-runtime": "^0.5.3",
52
52
  "@typesafe-ai/sdk": "0.6.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/bun": "1.3.14",
56
- "@gobing-ai/ts-db": "^0.5.2"
56
+ "@gobing-ai/ts-db": "^0.5.3"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"
@@ -66,6 +66,14 @@ const AUTH_PATTERNS: Partial<Record<AgentName, { positive: RegExp; negative: Reg
66
66
  positive: /(^|[^a-z])ok([^a-z]|$)|healthy|configured|ready/i,
67
67
  negative: /not[\s_-]*(configured|healthy|ok)|unhealthy|missing[\s_-]+dependenc|error|failed/i,
68
68
  },
69
+ // fm reports model availability, not login state: doctor "authenticated"
70
+ // means "system model available". The negative test runs first in
71
+ // probeAuthOutput, so `System model unavailable: <reason>` (exit 1) never
72
+ // false-matches the positive phrase.
73
+ fm: {
74
+ positive: /System model available/i,
75
+ negative: /unavailable/i,
76
+ },
69
77
  };
70
78
 
71
79
  /** True when a value is a defined, non-blank string. */
@@ -1,4 +1,5 @@
1
1
  import { getLogger } from '@gobing-ai/ts-infra';
2
+ import { joinPath } from '@gobing-ai/ts-runtime';
2
3
 
3
4
  /** Identifier for one supported coding agent (canonical id). */
4
5
  export type AgentName =
@@ -12,7 +13,8 @@ export type AgentName =
12
13
  | 'hermes'
13
14
  | 'omp'
14
15
  | 'grok'
15
- | 'deepseek';
16
+ | 'deepseek'
17
+ | 'fm';
16
18
 
17
19
  /** Output mode for prompt invocations. */
18
20
  export type OutputMode = 'text' | 'json';
@@ -110,6 +112,8 @@ export interface AgentShim {
110
112
  readonly aliases?: readonly string[];
111
113
  /** Deprecation marker; resolving a deprecated id warns and reports canonical status. */
112
114
  readonly deprecated?: AgentDeprecation;
115
+ /** Prompt-only LLM: no file-writing or shell tools. Callers dispatching tool-using work must refuse it. */
116
+ readonly textOnly?: boolean;
113
117
  /** Build a help-display command. */
114
118
  getHelpCommand(): ShimCommand;
115
119
  /** Build a version-detection command. */
@@ -419,6 +423,56 @@ const dshShim: AgentShim = {
419
423
  getAuthCommand: () => null,
420
424
  };
421
425
 
426
+ /**
427
+ * Apple Foundation Model CLI (`/usr/bin/fm`, macOS 27) — tier-1, text-only.
428
+ * A plain on-device LLM with no file or shell tools, so it must never be
429
+ * auto-selected for tool-using work (it is deliberately absent from
430
+ * `TIER1_PRIORITY`; the consumer is spur `agent-service.ts`).
431
+ *
432
+ * No `--version` flag (`fm --version` prints "Unknown option" and exits 0),
433
+ * so the version comes from the binary's embedded SCCS build string via
434
+ * `what -q /usr/bin/fm` — absolute path deliberate, `fm` ships only there;
435
+ * `what` exits 1 when the file is absent, so Linux/older macOS report
436
+ * not-installed through the existing non-zero-exit branch.
437
+ *
438
+ * Auth is the system-model availability probe; bare `fm available` always
439
+ * exits 0 and is never used. The transcript file is the session (ADR-047 R5):
440
+ * `--resume <file> --save-transcript <file>` resumes it, a missing transcript
441
+ * makes `fm` exit 1 ("Unable to read transcript at …") which surfaces as a
442
+ * failed run, never a silent fresh start. The shim stays pure — no
443
+ * filesystem I/O. Structured output (`--schema`) belongs to ts-decision-fm.
444
+ */
445
+ const fmShim: AgentShim = {
446
+ name: 'fm',
447
+ command: 'fm',
448
+ tier: 1,
449
+ textOnly: true,
450
+ getHelpCommand: () => ({ command: 'fm', args: ['--help'] }),
451
+ getVersionCommand: () => ({ command: 'what', args: ['-q', '/usr/bin/fm'] }),
452
+ getPromptCommand: (options) => {
453
+ // Headless one-shot text response. mode is ignored: `json` has no
454
+ // schema to pass through the generic PromptOptions (that surface is
455
+ // ts-decision-fm's `--schema` bridge).
456
+ const args = ['respond', '--no-stream'];
457
+ if (options.model !== undefined) args.push('-m', options.model);
458
+ // ADR-047 R5: sessionId/sessionDir select the scoped transcript file
459
+ // and suppress any unscoped continue. `continue` alone degrades to a
460
+ // fresh call — fm has no implicit last-session flag.
461
+ if (options.sessionId !== undefined) {
462
+ const file =
463
+ options.sessionDir !== undefined
464
+ ? joinPath(options.sessionDir, `${options.sessionId}.json`)
465
+ : `${options.sessionId}.json`;
466
+ args.push('--resume', file, '--save-transcript', file);
467
+ } else if (options.sessionDir !== undefined) {
468
+ args.push('--save-transcript', joinPath(options.sessionDir, 'fm-session.json'));
469
+ }
470
+ args.push(options.input ?? '');
471
+ return { command: 'fm', args };
472
+ },
473
+ getAuthCommand: () => ({ command: 'fm', args: ['available', '--model', 'system'] }),
474
+ };
475
+
422
476
  /** All bundled agent shims keyed by canonical agent name. */
423
477
  export const AGENT_SHIMS: Readonly<Record<AgentName, AgentShim>> = {
424
478
  claude: claudeShim,
@@ -432,6 +486,7 @@ export const AGENT_SHIMS: Readonly<Record<AgentName, AgentShim>> = {
432
486
  omp: ompShim,
433
487
  grok: grokShim,
434
488
  deepseek: dshShim,
489
+ fm: fmShim,
435
490
  };
436
491
 
437
492
  /** Session-affinity capability for one coding agent (ADR-047). */
@@ -559,6 +614,14 @@ const AGENT_SESSION_CAPABILITY: Readonly<Record<AgentName, AgentSessionCapabilit
559
614
  verifiedAgainst: '0.1.5-rc.1',
560
615
  note: 'headless app takes only the task positional and -h at 0.1.5-rc.1 — no resume/session/mode flags; fresh-dispatch degrade',
561
616
  },
617
+ fm: {
618
+ supportsResumeById: true,
619
+ supportsSessionDir: true,
620
+ supportsPersistentStdin: false,
621
+ supportsStructuredOutput: false,
622
+ verifiedAgainst: 'FoundationModels-2.0.68.1.402',
623
+ note: '`fm chat` is interactive only — no multi-turn stdin input mode; structured output needs `--schema <file>`, which the generic PromptOptions cannot carry (ts-decision-fm surface)',
624
+ },
562
625
  };
563
626
 
564
627
  /** Query a bundled agent's session-affinity capability by canonical name. */
@@ -592,6 +655,7 @@ export const DISPLAY_ORDER: readonly AgentName[] = [
592
655
  'hermes',
593
656
  'grok',
594
657
  'deepseek',
658
+ 'fm',
595
659
  ];
596
660
 
597
661
  /** Set of gateway/TUI-constrained agents. */
@@ -42,8 +42,8 @@ export interface DecisionMaker {
42
42
  noul(state: DecisionState, prompt?: Desc, outcomes?: { yes?: Desc; no?: Desc }): Promise<NoulAnswer>;
43
43
  }
44
44
 
45
- /** Available named decision backends (task 0080). */
46
- export type DecisionBackend = 'typesafe' | 'laya-local';
45
+ /** Available named decision backends (task 0080, task 0085). */
46
+ export type DecisionBackend = 'typesafe' | 'laya-local' | 'fm-local';
47
47
 
48
48
  /** Factory options: driver selection, credential injection, and transport tuning. */
49
49
  export interface DecisionMakerOptions {
@@ -101,6 +101,26 @@ async function resolveLayaDriver(options: DecisionMakerOptions): Promise<Decisio
101
101
  }
102
102
  }
103
103
 
104
+ const FM_DRIVER_PACKAGE = '@gobing-ai/ts-decision-fm';
105
+
106
+ async function resolveFmDriver(options: DecisionMakerOptions): Promise<DecisionDriver> {
107
+ try {
108
+ const mod = (await import(FM_DRIVER_PACKAGE)) as {
109
+ createFmDriver?: (opts?: unknown) => DecisionDriver;
110
+ };
111
+ if (typeof mod.createFmDriver !== 'function') {
112
+ throw new Error(`Module '${FM_DRIVER_PACKAGE}' does not export createFmDriver`);
113
+ }
114
+ return mod.createFmDriver(options);
115
+ } catch (cause) {
116
+ throw new DecisionConfigError(
117
+ `The 'fm-local' backend requires '${FM_DRIVER_PACKAGE}' to be installed; install it with 'bun add ${FM_DRIVER_PACKAGE}'`,
118
+ 'FM_BACKEND',
119
+ { cause },
120
+ );
121
+ }
122
+ }
123
+
104
124
  /**
105
125
  * Build a DecisionMaker. `options.driver` wins when supplied; otherwise `options.backend`
106
126
  * resolves the driver (default `'typesafe'`). The driver is constructed lazily on first use.
@@ -126,6 +146,10 @@ export function createDecisionMaker(options: DecisionMakerOptions = {}): Decisio
126
146
  resolvedDriver = await resolveLayaDriver(options);
127
147
  return resolvedDriver;
128
148
  }
149
+ if (backend === 'fm-local') {
150
+ resolvedDriver = await resolveFmDriver(options);
151
+ return resolvedDriver;
152
+ }
129
153
  throw new DecisionConfigError(`Unknown decision backend '${backend}'`, 'backend');
130
154
  };
131
155