@gobing-ai/ts-ai-runner 0.5.2 → 0.5.4
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 +9 -1
- package/dist/agents/auth-shims.d.ts.map +1 -1
- package/dist/agents/auth-shims.js +8 -0
- package/dist/agents/shims.d.ts +3 -1
- package/dist/agents/shims.d.ts.map +1 -1
- package/dist/agents/shims.js +61 -0
- package/dist/decision/decision-maker.d.ts +2 -2
- package/dist/decision/decision-maker.d.ts.map +1 -1
- package/dist/decision/decision-maker.js +17 -0
- package/package.json +4 -4
- package/src/agents/auth-shims.ts +8 -0
- package/src/agents/shims.ts +65 -1
- package/src/decision/decision-maker.ts +26 -2
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
|
|
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;
|
|
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) {
|
package/dist/agents/shims.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/agents/shims.ts"],"names":[],"mappings":"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"}
|
package/dist/agents/shims.js
CHANGED
|
@@ -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,
|
|
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.
|
|
3
|
+
"version": "0.5.4",
|
|
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.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
50
|
+
"@gobing-ai/ts-infra": "^0.5.4",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.5.4",
|
|
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.
|
|
56
|
+
"@gobing-ai/ts-db": "^0.5.4"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public"
|
package/src/agents/auth-shims.ts
CHANGED
|
@@ -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. */
|
package/src/agents/shims.ts
CHANGED
|
@@ -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
|
|