@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 +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 +65 -4
- package/dist/decision/decision-maker.d.ts +8 -5
- package/dist/decision/decision-maker.d.ts.map +1 -1
- package/dist/decision/decision-maker.js +55 -19
- package/package.json +4 -4
- package/src/agents/auth-shims.ts +8 -0
- package/src/agents/shims.ts +69 -5
- package/src/decision/decision-maker.ts +75 -20
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
|
|
@@ -356,14 +408,14 @@ const AGENT_SESSION_CAPABILITY = {
|
|
|
356
408
|
supportsSessionDir: true,
|
|
357
409
|
supportsPersistentStdin: true,
|
|
358
410
|
supportsStructuredOutput: true,
|
|
359
|
-
verifiedAgainst: '0.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
40
|
-
*
|
|
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,
|
|
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
|
|
26
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
let defaultDriver;
|
|
34
|
-
const resolveDriver = () => {
|
|
55
|
+
let resolvedDriver;
|
|
56
|
+
const getDriver = async () => {
|
|
35
57
|
if (options.driver)
|
|
36
58
|
return options.driver;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
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.
|
|
56
|
+
"@gobing-ai/ts-db": "^0.5.3"
|
|
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). */
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
80
|
-
*
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 =
|
|
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
|