@gobing-ai/ts-decision-fm 0.5.4 → 0.5.7

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/dist/driver.js CHANGED
@@ -29,7 +29,7 @@ export function createFmDriver(options = {}) {
29
29
  async function ensureFmAvailable() {
30
30
  if (availabilityVerified)
31
31
  return;
32
- await probeFmAvailability(executor, fmPath);
32
+ await probeFmAvailability(executor, fmPath, requestTimeoutMs);
33
33
  availabilityVerified = true;
34
34
  }
35
35
  return {
@@ -59,7 +59,7 @@ export function createFmDriver(options = {}) {
59
59
  const instructions = FM_INSTRUCTIONS;
60
60
  const prompt = buildPrompt(state, questions);
61
61
  // R4: pre-flight budget — over budget rejects before any `fm respond`.
62
- const tokens = await countPromptTokens(executor, fmPath, instructions, prompt);
62
+ const tokens = await countPromptTokens(executor, fmPath, instructions, prompt, requestTimeoutMs);
63
63
  if (tokens > maxPromptTokens) {
64
64
  throw new DecisionRequestError(`prompt is ${tokens} tokens, above the maxPromptTokens budget of ${maxPromptTokens}`, undefined, undefined);
65
65
  }
@@ -9,9 +9,14 @@
9
9
  import type { ProcessExecutor } from '@gobing-ai/ts-runtime';
10
10
  /** `--guardrails` levels accepted by `fm respond` (design: fm CLI facts). */
11
11
  export type FmGuardrails = 'default' | 'permissive-content-transformations';
12
- /** Subcommand + flag order verified against the design's live observations. */
12
+ /**
13
+ * Subcommand + flag order verified against the design's live observations
14
+ * (task 0086 R4): instructions ride in `--instructions=<value>` and the prompt
15
+ * travels after a `--` separator, so instruction/prompt text can never be
16
+ * reparsed as fm flags regardless of its content.
17
+ */
13
18
  export declare function countTokensArgv(instructions: string, prompt: string): string[];
14
- /** Builds the `fm respond` argv: schema flag, `-i` instructions, positional prompt, optional `-g`/`--guardrails`. */
19
+ /** Builds the `fm respond` argv: schema flag, `--instructions=<instructions>`, `--` separator, positional prompt, optional `-g`/`--guardrails`. */
15
20
  export declare function respondArgv(options: {
16
21
  schemaPath: string;
17
22
  instructions: string;
@@ -27,14 +32,17 @@ export declare function availableArgv(): string[];
27
32
  * R8 prerequisite probe: `fm available --model system`. A spawn failure maps to
28
33
  * `DecisionConfigError` "fm not found"; a non-zero exit carries fm's printed
29
34
  * reason (e.g. `modelNotReady`). An exit 0 caches availability in the driver.
35
+ * The probe carries `timeoutMs` (task 0086 R12) so a hung fm cannot wedge the
36
+ * driver before the per-sample deadline exists.
30
37
  */
31
- export declare function probeFmAvailability(executor: ProcessExecutor, fmPath: string): Promise<void>;
38
+ export declare function probeFmAvailability(executor: ProcessExecutor, fmPath: string, timeoutMs?: number): Promise<void>;
32
39
  /**
33
- * R4 pre-flight: `fm count-tokens -q -i <instructions> <prompt>` prints a bare
34
- * integer. Non-zero exit or unparseable output is a `DecisionBackendError` —
35
- * the budget comparison itself happens in the driver.
40
+ * R4 pre-flight: `fm count-tokens -q --instructions=<instructions> -- <prompt>`
41
+ * prints a bare integer. Non-zero exit or unparseable output is a
42
+ * `DecisionBackendError` — the budget comparison itself happens in the driver.
43
+ * Carries `timeoutMs` (task 0086 R12); a timeout maps to `DecisionTimeoutError`.
36
44
  */
37
- export declare function countPromptTokens(executor: ProcessExecutor, fmPath: string, instructions: string, prompt: string): Promise<number>;
45
+ export declare function countPromptTokens(executor: ProcessExecutor, fmPath: string, instructions: string, prompt: string, timeoutMs?: number): Promise<number>;
38
46
  /**
39
47
  * One `fm respond` sample: run with the per-sample timeout, map every failure
40
48
  * row of the error table, return stdout on success (parsing happens in
@@ -1 +1 @@
1
- {"version":3,"file":"fm-process.d.ts","sourceRoot":"","sources":["../src/fm-process.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,uBAAuB,CAAC;AAE5E,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,oCAAoC,CAAC;AAE5E,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9E;AAED,qHAAqH;AACrH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC7B,GAAG,MAAM,EAAE,CAYX;AAED,kFAAkF;AAClF,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAExC;AAaD;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlG;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACnC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAMD;;;;GAIG;AACH,wBAAsB,YAAY,CAC9B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAAE,EACd,gBAAgB,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrE;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAShH"}
1
+ {"version":3,"file":"fm-process.d.ts","sourceRoot":"","sources":["../src/fm-process.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,uBAAuB,CAAC;AAE5E,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,oCAAoC,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9E;AAED,mJAAmJ;AACnJ,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC7B,GAAG,MAAM,EAAE,CAYX;AAED,kFAAkF;AAClF,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAExC;AAaD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACrC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,SAAS,SAAS,GACnB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACnC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,SAAS,GACnB,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAMD;;;;GAIG;AACH,wBAAsB,YAAY,CAC9B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAAE,EACd,gBAAgB,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrE;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAShH"}
@@ -7,21 +7,26 @@
7
7
  * token budget, and the pre-flight `count-tokens` check keeps it bounded.
8
8
  */
9
9
  import { DecisionBackendError, DecisionConfigError, DecisionRequestError, DecisionTimeoutError, } from '@gobing-ai/ts-ai-runner';
10
- /** Subcommand + flag order verified against the design's live observations. */
10
+ /**
11
+ * Subcommand + flag order verified against the design's live observations
12
+ * (task 0086 R4): instructions ride in `--instructions=<value>` and the prompt
13
+ * travels after a `--` separator, so instruction/prompt text can never be
14
+ * reparsed as fm flags regardless of its content.
15
+ */
11
16
  export function countTokensArgv(instructions, prompt) {
12
- return ['count-tokens', '-q', '-i', instructions, prompt];
17
+ return ['count-tokens', '-q', `--instructions=${instructions}`, '--', prompt];
13
18
  }
14
- /** Builds the `fm respond` argv: schema flag, `-i` instructions, positional prompt, optional `-g`/`--guardrails`. */
19
+ /** Builds the `fm respond` argv: schema flag, `--instructions=<instructions>`, `--` separator, positional prompt, optional `-g`/`--guardrails`. */
15
20
  export function respondArgv(options) {
16
21
  return [
17
22
  'respond',
18
23
  '--no-stream',
19
24
  '--schema',
20
25
  options.schemaPath,
21
- '-i',
22
- options.instructions,
26
+ `--instructions=${options.instructions}`,
23
27
  ...(options.guardrails !== undefined ? ['--guardrails', options.guardrails] : []),
24
28
  ...(options.greedy ? ['-g'] : []),
29
+ '--',
25
30
  options.prompt,
26
31
  ];
27
32
  }
@@ -42,9 +47,14 @@ function fmText(result) {
42
47
  * R8 prerequisite probe: `fm available --model system`. A spawn failure maps to
43
48
  * `DecisionConfigError` "fm not found"; a non-zero exit carries fm's printed
44
49
  * reason (e.g. `modelNotReady`). An exit 0 caches availability in the driver.
50
+ * The probe carries `timeoutMs` (task 0086 R12) so a hung fm cannot wedge the
51
+ * driver before the per-sample deadline exists.
45
52
  */
46
- export async function probeFmAvailability(executor, fmPath) {
47
- const result = await executor.run({ command: fmPath, args: availableArgv() });
53
+ export async function probeFmAvailability(executor, fmPath, timeoutMs = 10_000) {
54
+ const result = await executor.run({ command: fmPath, args: availableArgv(), timeout: timeoutMs });
55
+ if (result.outcome === 'timeout') {
56
+ throw new DecisionTimeoutError(`fm available exceeded requestTimeoutMs (${timeoutMs})`, timeoutMs);
57
+ }
48
58
  if (isSpawnFailure(result)) {
49
59
  throw new DecisionConfigError(`fm not found: '${fmPath}' could not be spawned`, 'fmPath');
50
60
  }
@@ -53,12 +63,20 @@ export async function probeFmAvailability(executor, fmPath) {
53
63
  }
54
64
  }
55
65
  /**
56
- * R4 pre-flight: `fm count-tokens -q -i <instructions> <prompt>` prints a bare
57
- * integer. Non-zero exit or unparseable output is a `DecisionBackendError` —
58
- * the budget comparison itself happens in the driver.
66
+ * R4 pre-flight: `fm count-tokens -q --instructions=<instructions> -- <prompt>`
67
+ * prints a bare integer. Non-zero exit or unparseable output is a
68
+ * `DecisionBackendError` — the budget comparison itself happens in the driver.
69
+ * Carries `timeoutMs` (task 0086 R12); a timeout maps to `DecisionTimeoutError`.
59
70
  */
60
- export async function countPromptTokens(executor, fmPath, instructions, prompt) {
61
- const result = await executor.run({ command: fmPath, args: countTokensArgv(instructions, prompt) });
71
+ export async function countPromptTokens(executor, fmPath, instructions, prompt, timeoutMs = 10_000) {
72
+ const result = await executor.run({
73
+ command: fmPath,
74
+ args: countTokensArgv(instructions, prompt),
75
+ timeout: timeoutMs,
76
+ });
77
+ if (result.outcome === 'timeout') {
78
+ throw new DecisionTimeoutError(`fm count-tokens exceeded requestTimeoutMs (${timeoutMs})`, timeoutMs);
79
+ }
62
80
  if (isSpawnFailure(result)) {
63
81
  throw new DecisionConfigError(`fm not found: '${fmPath}' could not be spawned`, 'fmPath');
64
82
  }
@@ -82,7 +100,8 @@ const GUARDRAILS_MARKER = 'safety guardrails were triggered';
82
100
  export async function runFmRespond(executor, fmPath, args, requestTimeoutMs) {
83
101
  const result = await executor.run({ command: fmPath, args, timeout: requestTimeoutMs });
84
102
  if (result.outcome === 'timeout') {
85
- throw new DecisionTimeoutError(`fm respond exceeded requestTimeoutMs (${requestTimeoutMs}): ${fmPath} ${args.join(' ')}`, requestTimeoutMs);
103
+ // Message deliberately drops the argv — the prompt is user content (task 0086 R12).
104
+ throw new DecisionTimeoutError(`fm ${args[0]} exceeded requestTimeoutMs (${requestTimeoutMs})`, requestTimeoutMs);
86
105
  }
87
106
  if (isSpawnFailure(result)) {
88
107
  throw new DecisionConfigError(`fm not found: '${fmPath}' could not be spawned`, 'fmPath');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-decision-fm",
3
- "version": "0.5.4",
3
+ "version": "0.5.7",
4
4
  "description": "@gobing-ai/ts-decision-fm — Apple on-device Foundation Model decision backend driving the macOS `fm` CLI over a one-shot process bridge.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -51,8 +51,8 @@
51
51
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-decision-fm-v<version> && git push --tags' && exit 1"
52
52
  },
53
53
  "dependencies": {
54
- "@gobing-ai/ts-ai-runner": "^0.5.4",
55
- "@gobing-ai/ts-runtime": "^0.5.4"
54
+ "@gobing-ai/ts-ai-runner": "^0.5.7",
55
+ "@gobing-ai/ts-runtime": "^0.5.7"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/bun": "1.3.14"
package/src/driver.ts CHANGED
@@ -97,7 +97,7 @@ export function createFmDriver(options: FmDriverOptions = {}): FmDecisionDriver
97
97
 
98
98
  async function ensureFmAvailable(): Promise<void> {
99
99
  if (availabilityVerified) return;
100
- await probeFmAvailability(executor, fmPath);
100
+ await probeFmAvailability(executor, fmPath, requestTimeoutMs);
101
101
  availabilityVerified = true;
102
102
  }
103
103
 
@@ -149,7 +149,7 @@ export function createFmDriver(options: FmDriverOptions = {}): FmDecisionDriver
149
149
  const prompt = buildPrompt(state, questions);
150
150
 
151
151
  // R4: pre-flight budget — over budget rejects before any `fm respond`.
152
- const tokens = await countPromptTokens(executor, fmPath, instructions, prompt);
152
+ const tokens = await countPromptTokens(executor, fmPath, instructions, prompt, requestTimeoutMs);
153
153
  if (tokens > maxPromptTokens) {
154
154
  throw new DecisionRequestError(
155
155
  `prompt is ${tokens} tokens, above the maxPromptTokens budget of ${maxPromptTokens}`,
package/src/fm-process.ts CHANGED
@@ -18,12 +18,17 @@ import type { ProcessExecutor, ProcessResult } from '@gobing-ai/ts-runtime';
18
18
  /** `--guardrails` levels accepted by `fm respond` (design: fm CLI facts). */
19
19
  export type FmGuardrails = 'default' | 'permissive-content-transformations';
20
20
 
21
- /** Subcommand + flag order verified against the design's live observations. */
21
+ /**
22
+ * Subcommand + flag order verified against the design's live observations
23
+ * (task 0086 R4): instructions ride in `--instructions=<value>` and the prompt
24
+ * travels after a `--` separator, so instruction/prompt text can never be
25
+ * reparsed as fm flags regardless of its content.
26
+ */
22
27
  export function countTokensArgv(instructions: string, prompt: string): string[] {
23
- return ['count-tokens', '-q', '-i', instructions, prompt];
28
+ return ['count-tokens', '-q', `--instructions=${instructions}`, '--', prompt];
24
29
  }
25
30
 
26
- /** Builds the `fm respond` argv: schema flag, `-i` instructions, positional prompt, optional `-g`/`--guardrails`. */
31
+ /** Builds the `fm respond` argv: schema flag, `--instructions=<instructions>`, `--` separator, positional prompt, optional `-g`/`--guardrails`. */
27
32
  export function respondArgv(options: {
28
33
  schemaPath: string;
29
34
  instructions: string;
@@ -38,10 +43,10 @@ export function respondArgv(options: {
38
43
  '--no-stream',
39
44
  '--schema',
40
45
  options.schemaPath,
41
- '-i',
42
- options.instructions,
46
+ `--instructions=${options.instructions}`,
43
47
  ...(options.guardrails !== undefined ? ['--guardrails', options.guardrails] : []),
44
48
  ...(options.greedy ? ['-g'] : []),
49
+ '--',
45
50
  options.prompt,
46
51
  ];
47
52
  }
@@ -66,9 +71,18 @@ function fmText(result: ProcessResult): string {
66
71
  * R8 prerequisite probe: `fm available --model system`. A spawn failure maps to
67
72
  * `DecisionConfigError` "fm not found"; a non-zero exit carries fm's printed
68
73
  * reason (e.g. `modelNotReady`). An exit 0 caches availability in the driver.
74
+ * The probe carries `timeoutMs` (task 0086 R12) so a hung fm cannot wedge the
75
+ * driver before the per-sample deadline exists.
69
76
  */
70
- export async function probeFmAvailability(executor: ProcessExecutor, fmPath: string): Promise<void> {
71
- const result = await executor.run({ command: fmPath, args: availableArgv() });
77
+ export async function probeFmAvailability(
78
+ executor: ProcessExecutor,
79
+ fmPath: string,
80
+ timeoutMs = 10_000,
81
+ ): Promise<void> {
82
+ const result = await executor.run({ command: fmPath, args: availableArgv(), timeout: timeoutMs });
83
+ if (result.outcome === 'timeout') {
84
+ throw new DecisionTimeoutError(`fm available exceeded requestTimeoutMs (${timeoutMs})`, timeoutMs);
85
+ }
72
86
  if (isSpawnFailure(result)) {
73
87
  throw new DecisionConfigError(`fm not found: '${fmPath}' could not be spawned`, 'fmPath');
74
88
  }
@@ -78,17 +92,26 @@ export async function probeFmAvailability(executor: ProcessExecutor, fmPath: str
78
92
  }
79
93
 
80
94
  /**
81
- * R4 pre-flight: `fm count-tokens -q -i <instructions> <prompt>` prints a bare
82
- * integer. Non-zero exit or unparseable output is a `DecisionBackendError` —
83
- * the budget comparison itself happens in the driver.
95
+ * R4 pre-flight: `fm count-tokens -q --instructions=<instructions> -- <prompt>`
96
+ * prints a bare integer. Non-zero exit or unparseable output is a
97
+ * `DecisionBackendError` — the budget comparison itself happens in the driver.
98
+ * Carries `timeoutMs` (task 0086 R12); a timeout maps to `DecisionTimeoutError`.
84
99
  */
85
100
  export async function countPromptTokens(
86
101
  executor: ProcessExecutor,
87
102
  fmPath: string,
88
103
  instructions: string,
89
104
  prompt: string,
105
+ timeoutMs = 10_000,
90
106
  ): Promise<number> {
91
- const result = await executor.run({ command: fmPath, args: countTokensArgv(instructions, prompt) });
107
+ const result = await executor.run({
108
+ command: fmPath,
109
+ args: countTokensArgv(instructions, prompt),
110
+ timeout: timeoutMs,
111
+ });
112
+ if (result.outcome === 'timeout') {
113
+ throw new DecisionTimeoutError(`fm count-tokens exceeded requestTimeoutMs (${timeoutMs})`, timeoutMs);
114
+ }
92
115
  if (isSpawnFailure(result)) {
93
116
  throw new DecisionConfigError(`fm not found: '${fmPath}' could not be spawned`, 'fmPath');
94
117
  }
@@ -125,8 +148,9 @@ export async function runFmRespond(
125
148
  ): Promise<string> {
126
149
  const result = await executor.run({ command: fmPath, args, timeout: requestTimeoutMs });
127
150
  if (result.outcome === 'timeout') {
151
+ // Message deliberately drops the argv — the prompt is user content (task 0086 R12).
128
152
  throw new DecisionTimeoutError(
129
- `fm respond exceeded requestTimeoutMs (${requestTimeoutMs}): ${fmPath} ${args.join(' ')}`,
153
+ `fm ${args[0]} exceeded requestTimeoutMs (${requestTimeoutMs})`,
130
154
  requestTimeoutMs,
131
155
  );
132
156
  }