@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 +2 -2
- package/dist/fm-process.d.ts +15 -7
- package/dist/fm-process.d.ts.map +1 -1
- package/dist/fm-process.js +32 -13
- package/package.json +3 -3
- package/src/driver.ts +2 -2
- package/src/fm-process.ts +36 -12
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
|
}
|
package/dist/fm-process.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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,
|
|
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
|
|
34
|
-
* integer. Non-zero exit or unparseable output is a
|
|
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
|
package/dist/fm-process.d.ts.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/fm-process.js
CHANGED
|
@@ -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
|
-
/**
|
|
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', '
|
|
17
|
+
return ['count-tokens', '-q', `--instructions=${instructions}`, '--', prompt];
|
|
13
18
|
}
|
|
14
|
-
/** Builds the `fm respond` argv: schema flag,
|
|
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
|
-
|
|
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
|
|
57
|
-
* integer. Non-zero exit or unparseable output is a
|
|
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({
|
|
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
|
-
|
|
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.
|
|
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.
|
|
55
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
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
|
-
/**
|
|
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', '
|
|
28
|
+
return ['count-tokens', '-q', `--instructions=${instructions}`, '--', prompt];
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
/** Builds the `fm respond` argv: schema flag,
|
|
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
|
-
|
|
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(
|
|
71
|
-
|
|
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
|
|
82
|
-
* integer. Non-zero exit or unparseable output is a
|
|
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({
|
|
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
|
|
153
|
+
`fm ${args[0]} exceeded requestTimeoutMs (${requestTimeoutMs})`,
|
|
130
154
|
requestTimeoutMs,
|
|
131
155
|
);
|
|
132
156
|
}
|