@gobing-ai/ts-ai-runner 0.5.1 → 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
@@ -356,14 +408,14 @@ const AGENT_SESSION_CAPABILITY = {
356
408
  supportsSessionDir: true,
357
409
  supportsPersistentStdin: true,
358
410
  supportsStructuredOutput: true,
359
- verifiedAgainst: '0.85.1',
411
+ verifiedAgainst: '0.87.0',
360
412
  },
361
413
  claude: {
362
414
  supportsResumeById: true,
363
415
  supportsSessionDir: false,
364
416
  supportsPersistentStdin: true,
365
417
  supportsStructuredOutput: true,
366
- verifiedAgainst: '2.1.274',
418
+ verifiedAgainst: '2.1.278',
367
419
  // Persistent stdin is shim-wired: `-p --input-format stream-json --output-format
368
420
  // stream-json` keeps the process alive reading JSONL envelopes (verified --help;
369
421
  // --input-format only works with --print).
@@ -374,7 +426,7 @@ const AGENT_SESSION_CAPABILITY = {
374
426
  supportsSessionDir: false,
375
427
  supportsPersistentStdin: false,
376
428
  supportsStructuredOutput: true,
377
- verifiedAgainst: '0.154.0',
429
+ verifiedAgainst: '0.155.1',
378
430
  // R3 branch 2: `exec resume <id> <prompt>` is the working non-interactive
379
431
  // resume (verified 0.154.0) — wired in getPromptCommand below.
380
432
  note: 'no session-dir flag — sessionDir is ignored; `exec` carries one prompt arg (stdin `-` is one-shot), so no multi-turn stdin',
@@ -394,7 +446,7 @@ const AGENT_SESSION_CAPABILITY = {
394
446
  supportsSessionDir: false,
395
447
  supportsPersistentStdin: false,
396
448
  supportsStructuredOutput: true,
397
- verifiedAgainst: '1.0.34',
449
+ verifiedAgainst: '1.0.40',
398
450
  note: 'no session-dir flag — sessionDir is ignored (best-effort isolate); no multi-turn stdin input mode',
399
451
  },
400
452
  gemini: {
@@ -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,10 +19,14 @@ export interface DecisionMaker {
19
19
  no?: Desc;
20
20
  }): Promise<NoulAnswer>;
21
21
  }
22
+ /** Available named decision backends (task 0080, task 0085). */
23
+ export type DecisionBackend = 'typesafe' | 'laya-local' | 'fm-local';
22
24
  /** Factory options: driver selection, credential injection, and transport tuning. */
23
25
  export interface DecisionMakerOptions {
24
- /** Custom driver — when supplied, the default TypeSafe driver is never constructed and no key is required. */
26
+ /** Custom driver — when supplied, `backend` is ignored, no default driver is constructed and no key is required. */
25
27
  driver?: DecisionDriver;
28
+ /** Named backend selector. Default: `'typesafe'`. Resolution order: `driver` → `backend` → `'typesafe'`. */
29
+ backend?: DecisionBackend;
26
30
  /** Injected env record; defaults to `getProcessEnv()` (doctor-runner convention). */
27
31
  env?: Record<string, string | undefined>;
28
32
  /** Explicit key — wins over `env.TYPESAFE_API_KEY`. */
@@ -34,12 +38,11 @@ export interface DecisionMakerOptions {
34
38
  maxRetries?: number;
35
39
  /** Injected fetch for tests. */
36
40
  fetch?: typeof fetch;
41
+ [key: string]: unknown;
37
42
  }
38
43
  /**
39
- * Build a DecisionMaker. `options.driver` wins when supplied; otherwise the
40
- * TypeSafe driver is constructed lazily on first use, after key resolution —
41
- * so a custom driver never requires a key and a missing key fails before any
42
- * request (R6/R7).
44
+ * Build a DecisionMaker. `options.driver` wins when supplied; otherwise `options.backend`
45
+ * resolves the driver (default `'typesafe'`). The driver is constructed lazily on first use.
43
46
  */
44
47
  export declare function createDecisionMaker(options?: DecisionMakerOptions): DecisionMaker;
45
48
  //# sourceMappingURL=decision-maker.d.ts.map
@@ -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,qFAAqF;AACrF,MAAM,WAAW,oBAAoB;IACjC,8GAA8G;IAC9G,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,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;CACxB;AAkBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CA6CrF"}
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"}
@@ -21,33 +21,69 @@ function resolveApiKey(options) {
21
21
  }
22
22
  return apiKey;
23
23
  }
24
+ const LAYA_DRIVER_PACKAGE = '@gobing-ai/ts-laya-mlx';
25
+ async function resolveLayaDriver(options) {
26
+ try {
27
+ const mod = (await import(LAYA_DRIVER_PACKAGE));
28
+ if (typeof mod.createLayaDriver !== 'function') {
29
+ throw new Error(`Module '${LAYA_DRIVER_PACKAGE}' does not export createLayaDriver`);
30
+ }
31
+ return mod.createLayaDriver(options);
32
+ }
33
+ catch (cause) {
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
+ }
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
+ }
24
50
  /**
25
- * Build a DecisionMaker. `options.driver` wins when supplied; otherwise the
26
- * TypeSafe driver is constructed lazily on first use, after key resolution —
27
- * so a custom driver never requires a key and a missing key fails before any
28
- * request (R6/R7).
51
+ * Build a DecisionMaker. `options.driver` wins when supplied; otherwise `options.backend`
52
+ * resolves the driver (default `'typesafe'`). The driver is constructed lazily on first use.
29
53
  */
30
54
  export function createDecisionMaker(options = {}) {
31
- // Lazy driver slot: resolved on first use, at most once. A caller-supplied
32
- // driver never reaches key resolution or default-driver construction (R6).
33
- let defaultDriver;
34
- const resolveDriver = () => {
55
+ let resolvedDriver;
56
+ const getDriver = async () => {
35
57
  if (options.driver)
36
58
  return options.driver;
37
- defaultDriver ??= createTypesafeDriver({
38
- apiKey: resolveApiKey(options),
39
- model: options.model,
40
- baseURL: options.baseURL,
41
- timeoutMs: options.timeoutMs,
42
- maxRetries: options.maxRetries,
43
- fetch: options.fetch,
44
- });
45
- return defaultDriver;
59
+ if (resolvedDriver)
60
+ return resolvedDriver;
61
+ const backend = options.backend ?? 'typesafe';
62
+ if (backend === 'typesafe') {
63
+ resolvedDriver = createTypesafeDriver({
64
+ apiKey: resolveApiKey(options),
65
+ model: options.model,
66
+ baseURL: options.baseURL,
67
+ timeoutMs: options.timeoutMs,
68
+ maxRetries: options.maxRetries,
69
+ fetch: options.fetch,
70
+ });
71
+ return resolvedDriver;
72
+ }
73
+ if (backend === 'laya-local') {
74
+ resolvedDriver = await resolveLayaDriver(options);
75
+ return resolvedDriver;
76
+ }
77
+ if (backend === 'fm-local') {
78
+ resolvedDriver = await resolveFmDriver(options);
79
+ return resolvedDriver;
80
+ }
81
+ throw new DecisionConfigError(`Unknown decision backend '${backend}'`, 'backend');
46
82
  };
47
83
  const ask = async (req) => {
48
84
  // R3: the questions map reaches the driver untouched — same reference,
49
85
  // no reordering, renaming, or dropping.
50
- const driver = resolveDriver();
86
+ const driver = await getDriver();
51
87
  validateQuestions(req.questions);
52
88
  const answers = await driver.ask(req);
53
89
  validateAnswers(req.questions, answers);
@@ -55,7 +91,7 @@ export function createDecisionMaker(options = {}) {
55
91
  return answers;
56
92
  };
57
93
  return {
58
- driver: options.driver?.name ?? 'typesafe',
94
+ driver: options.driver?.name ?? options.backend ?? 'typesafe',
59
95
  ask,
60
96
  // R4/R5: each sugar method is one `ask` with exactly one `q.*` question,
61
97
  // unwrapping the single answer — no second driver call, no duplicated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-ai-runner",
3
- "version": "0.5.1",
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.1",
51
- "@gobing-ai/ts-runtime": "^0.5.1",
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.1"
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). */
@@ -477,14 +532,14 @@ const AGENT_SESSION_CAPABILITY: Readonly<Record<AgentName, AgentSessionCapabilit
477
532
  supportsSessionDir: true,
478
533
  supportsPersistentStdin: true,
479
534
  supportsStructuredOutput: true,
480
- verifiedAgainst: '0.85.1',
535
+ verifiedAgainst: '0.87.0',
481
536
  },
482
537
  claude: {
483
538
  supportsResumeById: true,
484
539
  supportsSessionDir: false,
485
540
  supportsPersistentStdin: true,
486
541
  supportsStructuredOutput: true,
487
- verifiedAgainst: '2.1.274',
542
+ verifiedAgainst: '2.1.278',
488
543
  // Persistent stdin is shim-wired: `-p --input-format stream-json --output-format
489
544
  // stream-json` keeps the process alive reading JSONL envelopes (verified --help;
490
545
  // --input-format only works with --print).
@@ -495,7 +550,7 @@ const AGENT_SESSION_CAPABILITY: Readonly<Record<AgentName, AgentSessionCapabilit
495
550
  supportsSessionDir: false,
496
551
  supportsPersistentStdin: false,
497
552
  supportsStructuredOutput: true,
498
- verifiedAgainst: '0.154.0',
553
+ verifiedAgainst: '0.155.1',
499
554
  // R3 branch 2: `exec resume <id> <prompt>` is the working non-interactive
500
555
  // resume (verified 0.154.0) — wired in getPromptCommand below.
501
556
  note: 'no session-dir flag — sessionDir is ignored; `exec` carries one prompt arg (stdin `-` is one-shot), so no multi-turn stdin',
@@ -515,7 +570,7 @@ const AGENT_SESSION_CAPABILITY: Readonly<Record<AgentName, AgentSessionCapabilit
515
570
  supportsSessionDir: false,
516
571
  supportsPersistentStdin: false,
517
572
  supportsStructuredOutput: true,
518
- verifiedAgainst: '1.0.34',
573
+ verifiedAgainst: '1.0.40',
519
574
  note: 'no session-dir flag — sessionDir is ignored (best-effort isolate); no multi-turn stdin input mode',
520
575
  },
521
576
  gemini: {
@@ -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,10 +42,15 @@ 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, task 0085). */
46
+ export type DecisionBackend = 'typesafe' | 'laya-local' | 'fm-local';
47
+
45
48
  /** Factory options: driver selection, credential injection, and transport tuning. */
46
49
  export interface DecisionMakerOptions {
47
- /** Custom driver — when supplied, the default TypeSafe driver is never constructed and no key is required. */
50
+ /** Custom driver — when supplied, `backend` is ignored, no default driver is constructed and no key is required. */
48
51
  driver?: DecisionDriver;
52
+ /** Named backend selector. Default: `'typesafe'`. Resolution order: `driver` → `backend` → `'typesafe'`. */
53
+ backend?: DecisionBackend;
49
54
  /** Injected env record; defaults to `getProcessEnv()` (doctor-runner convention). */
50
55
  env?: Record<string, string | undefined>;
51
56
  /** Explicit key — wins over `env.TYPESAFE_API_KEY`. */
@@ -57,6 +62,7 @@ export interface DecisionMakerOptions {
57
62
  maxRetries?: number;
58
63
  /** Injected fetch for tests. */
59
64
  fetch?: typeof fetch;
65
+ [key: string]: unknown;
60
66
  }
61
67
 
62
68
  /**
@@ -75,27 +81,76 @@ function resolveApiKey(options: DecisionMakerOptions): string {
75
81
  return apiKey;
76
82
  }
77
83
 
84
+ const LAYA_DRIVER_PACKAGE = '@gobing-ai/ts-laya-mlx';
85
+
86
+ async function resolveLayaDriver(options: DecisionMakerOptions): Promise<DecisionDriver> {
87
+ try {
88
+ const mod = (await import(LAYA_DRIVER_PACKAGE)) as {
89
+ createLayaDriver?: (opts?: unknown) => DecisionDriver;
90
+ };
91
+ if (typeof mod.createLayaDriver !== 'function') {
92
+ throw new Error(`Module '${LAYA_DRIVER_PACKAGE}' does not export createLayaDriver`);
93
+ }
94
+ return mod.createLayaDriver(options);
95
+ } catch (cause) {
96
+ throw new DecisionConfigError(
97
+ `The 'laya-local' backend requires '${LAYA_DRIVER_PACKAGE}' to be installed; install it with 'bun add ${LAYA_DRIVER_PACKAGE}'`,
98
+ 'LAYA_BACKEND',
99
+ { cause },
100
+ );
101
+ }
102
+ }
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
+
78
124
  /**
79
- * Build a DecisionMaker. `options.driver` wins when supplied; otherwise the
80
- * TypeSafe driver is constructed lazily on first use, after key resolution —
81
- * so a custom driver never requires a key and a missing key fails before any
82
- * request (R6/R7).
125
+ * Build a DecisionMaker. `options.driver` wins when supplied; otherwise `options.backend`
126
+ * resolves the driver (default `'typesafe'`). The driver is constructed lazily on first use.
83
127
  */
84
128
  export function createDecisionMaker(options: DecisionMakerOptions = {}): DecisionMaker {
85
- // Lazy driver slot: resolved on first use, at most once. A caller-supplied
86
- // driver never reaches key resolution or default-driver construction (R6).
87
- let defaultDriver: DecisionDriver | undefined;
88
- const resolveDriver = (): DecisionDriver => {
129
+ let resolvedDriver: DecisionDriver | undefined;
130
+ const getDriver = async (): Promise<DecisionDriver> => {
89
131
  if (options.driver) return options.driver;
90
- defaultDriver ??= createTypesafeDriver({
91
- apiKey: resolveApiKey(options),
92
- model: options.model,
93
- baseURL: options.baseURL,
94
- timeoutMs: options.timeoutMs,
95
- maxRetries: options.maxRetries,
96
- fetch: options.fetch,
97
- });
98
- return defaultDriver;
132
+ if (resolvedDriver) return resolvedDriver;
133
+ const backend = options.backend ?? 'typesafe';
134
+ if (backend === 'typesafe') {
135
+ resolvedDriver = createTypesafeDriver({
136
+ apiKey: resolveApiKey(options),
137
+ model: options.model,
138
+ baseURL: options.baseURL,
139
+ timeoutMs: options.timeoutMs,
140
+ maxRetries: options.maxRetries,
141
+ fetch: options.fetch,
142
+ });
143
+ return resolvedDriver;
144
+ }
145
+ if (backend === 'laya-local') {
146
+ resolvedDriver = await resolveLayaDriver(options);
147
+ return resolvedDriver;
148
+ }
149
+ if (backend === 'fm-local') {
150
+ resolvedDriver = await resolveFmDriver(options);
151
+ return resolvedDriver;
152
+ }
153
+ throw new DecisionConfigError(`Unknown decision backend '${backend}'`, 'backend');
99
154
  };
100
155
 
101
156
  const ask = async <const Q extends Record<string, Question>>(req: {
@@ -105,7 +160,7 @@ export function createDecisionMaker(options: DecisionMakerOptions = {}): Decisio
105
160
  }): Promise<AnswersFor<Q>> => {
106
161
  // R3: the questions map reaches the driver untouched — same reference,
107
162
  // no reordering, renaming, or dropping.
108
- const driver = resolveDriver();
163
+ const driver = await getDriver();
109
164
  validateQuestions(req.questions);
110
165
  const answers = await driver.ask(req);
111
166
  validateAnswers(req.questions, answers);
@@ -114,7 +169,7 @@ export function createDecisionMaker(options: DecisionMakerOptions = {}): Decisio
114
169
  };
115
170
 
116
171
  return {
117
- driver: options.driver?.name ?? 'typesafe',
172
+ driver: options.driver?.name ?? options.backend ?? 'typesafe',
118
173
  ask,
119
174
  // R4/R5: each sugar method is one `ask` with exactly one `q.*` question,
120
175
  // unwrapping the single answer — no second driver call, no duplicated