llm-output-guard 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,9 +73,12 @@ every detector, running on your own pasted output. No API key, no request.
73
73
  | `SCRIPT_MISMATCH` | Answered in the wrong alphabet | Share of letters outside expected scripts · opt-in |
74
74
  | `LANG_MISMATCH` | Wrong language, same alphabet | Function-word profile · opt-in |
75
75
  | `PROMPT_ECHO` | Returned your prompt instead of an answer | Share of output copied from the prompt · opt-in |
76
+ | `AGENT_LOOP` | An agent run circling instead of advancing | Exact periodicity across turns · `llm-output-guard/agent` |
76
77
 
77
78
  Every detector runs even after one fails, so a verdict shows the whole picture
78
- rather than whichever check happened to be ordered first. Each returns **0–1, not
79
+ rather than whichever check happened to be ordered first. All but the last read
80
+ one response; `AGENT_LOOP` reads a *sequence* of them and comes from its own
81
+ entry point, so `checkOutput` never returns it. Each returns **0–1, not
79
82
  a boolean** — you pick the line.
80
83
 
81
84
  **Full reference, with the measurements behind every default:
@@ -112,16 +115,23 @@ sequence.
112
115
 
113
116
  ```ts
114
117
  import { createAgentGuard } from 'llm-output-guard/agent';
118
+ import { toTurn } from 'llm-output-guard/openai';
115
119
 
116
120
  const guard = createAgentGuard();
117
121
 
118
122
  while (!done) {
119
- const response = await model.step();
120
- const verdict = guard.observe({ text: response.text, toolCalls: response.toolCalls });
123
+ const completion = await client.chat.completions.create(params);
124
+ const verdict = guard.observe(toTurn(completion));
121
125
  if (!verdict.ok) break; // the run is circling; stop paying for it
122
126
  }
123
127
  ```
124
128
 
129
+ `toTurn` ships from all four adapter subpaths and reads whatever that provider
130
+ sends — a JSON-string argument from OpenAI, a parsed object from Anthropic,
131
+ thought parts excluded on Gemini. Map a field by hand and get it wrong and
132
+ nothing throws: every turn fingerprints differently, the score pins at 0.000,
133
+ and you have a guard that never runs.
134
+
125
135
  `AGENT_LOOP` reads one axis nothing else here reads. It looks for an **exact**
126
136
  repeating cycle of turns, which is what keeps it off the shapes that dominate
127
137
  healthy agent traffic: twenty reads of twenty different files, an identical
@@ -132,6 +142,10 @@ A turn carrying tool calls is judged by its **arguments**, never its prose —
132
142
  that is the difference between an agent working through a list and an agent
133
143
  stuck on one item.
134
144
 
145
+ **[Try it on a run →](https://edwinsatya.github.io/llm-output-guard/)** — the
146
+ playground has an *agent run* mode: pick a trace, see which turns form the
147
+ cycle. The healthy traps are the half worth looking at.
148
+
135
149
  **[docs/agent-loops.md](docs/agent-loops.md)**
136
150
 
137
151
  ## The hard part is not catching loops
@@ -235,6 +249,13 @@ responses you already have, then derive thresholds you can defend:
235
249
  npx llm-output-guard check logs/*.txt --json | npx llm-output-guard calibrate --fpr 0.001
236
250
  ```
237
251
 
252
+ Agent runs calibrate the same way — `--trace` reads one run per line and scores
253
+ `AGENT_LOOP` instead of the per-response detectors:
254
+
255
+ ```bash
256
+ npx llm-output-guard check runs.jsonl --trace --json | npx llm-output-guard calibrate
257
+ ```
258
+
238
259
  `check` also works as a CI assertion — it exits 1 when anything is degenerate,
239
260
  2 when the input cannot be read.
240
261
 
@@ -250,7 +271,7 @@ because only one of those is evidence.
250
271
  - **Scores, not booleans.** Detectors report 0–1 and leave the threshold decision to you.
251
272
  - **Abstains rather than guesses.** Samples too short to judge score 0.
252
273
  - **Hand-rolled LZ77** rather than `node:zlib`, so the package stays runtime-agnostic.
253
- - **Sub-millisecond**, 0.383 ms at 500 B and 1.014 ms at 32 KB, with one detector accounting for most of it. `npm run bench` reproduces it — **[docs/performance.md](docs/performance.md)**
274
+ - **Sub-millisecond**, 0.383 ms at 500 B and 1.014 ms at 32 KB, with one detector accounting for most of it. A 12-turn agent run costs 0.075 ms. `npm run bench` reproduces both — **[docs/performance.md](docs/performance.md)**
254
275
  - **Chinese, Japanese and Thai** are handled where they differ: `TAIL_LOOP` switches to character mode, `REPETITION` is blind and says so — **[docs/script-coverage.md](docs/script-coverage.md)**
255
276
 
256
277
  ## Stability
@@ -261,7 +282,8 @@ the public surface was frozen export by export in that release.
261
282
  **The public API is** everything exported from `llm-output-guard`, plus
262
283
  `outputGuard` / `OutputGuardOptions` / `DegenerateAction` from `./ai-sdk`,
263
284
  `withOutputGuard` / `OutputGuardOptions` / `DegenerateAction` from `./openai`,
264
- `./anthropic` and `./google`, and `checkTrace` / `assertTrace` /
285
+ `./anthropic` and `./google`, `toTurn` from each of those four, and
286
+ `checkTrace` / `assertTrace` /
265
287
  `createAgentGuard` / `agentLoopScore` / `agentLoopDetail` from `./agent`. Each
266
288
  subpath is its own contract, so an option added to one is not
267
289
  a promise about the others. Anything else is internal and may move in any
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The shapes `./agent` reads.
3
+ *
4
+ * Structurally typed and provider-neutral, for the same reason the adapters
5
+ * are: a trace is assembled by the caller from whatever their framework hands
6
+ * back, and requiring a provider's own type here would make this subpath
7
+ * depend on a provider.
8
+ */
9
+ /** One tool call in a turn. */
10
+ interface AgentToolCall {
11
+ /**
12
+ * The tool's name. OpenAI spells it `function.name`, Anthropic and the AI SDK
13
+ * spell it `name`, and the caller maps whichever they have.
14
+ *
15
+ * Optional because a streamed call can arrive before its name does. A call
16
+ * with no name still fingerprints -- by its arguments -- rather than being
17
+ * dropped, since two nameless calls with identical arguments are still the
18
+ * same call twice.
19
+ */
20
+ name?: string | null;
21
+ /**
22
+ * The arguments, in any shape a provider sends: a parsed object, or the raw
23
+ * JSON string OpenAI uses. Both canonicalise to the same fingerprint, so a
24
+ * trace assembled from mixed sources still compares.
25
+ */
26
+ arguments?: unknown;
27
+ }
28
+ /** One model response in an agent run. */
29
+ interface AgentTurn {
30
+ /** The prose the model produced, if any. */
31
+ text?: string | null;
32
+ /**
33
+ * The tool calls the model issued, if any.
34
+ *
35
+ * **A turn carrying tool calls is judged by them alone**, and its `text` is
36
+ * read as a preamble and ignored -- the same rule the single-response
37
+ * adapters already apply. See `internal/turn-fingerprint.ts` for the trap
38
+ * that rule exists to avoid.
39
+ */
40
+ toolCalls?: readonly AgentToolCall[] | null;
41
+ }
42
+ interface AgentCheckOptions {
43
+ /**
44
+ * How many trailing turns to inspect. Default 12.
45
+ *
46
+ * The same idea as `TAIL_LOOP`'s tail: what matters is whether the agent is
47
+ * stuck *now*, not whether it repeated itself twenty turns ago and recovered.
48
+ * A trace shorter than the window is measured whole.
49
+ *
50
+ * It also bounds the longest cycle that can be found, at `window / minRepeats`
51
+ * turns -- 4 at the defaults. Raise both to catch longer orbits, and expect
52
+ * a loop at the very end of a long trace to score lower, because the score is
53
+ * coverage of the window rather than a count.
54
+ */
55
+ window?: number;
56
+ /**
57
+ * Turns required before this will judge at all. Default 4.
58
+ *
59
+ * Below it the detector abstains, which is the rule everywhere else in this
60
+ * package. Two identical turns is a retry, and three is a short poll; neither
61
+ * is evidence that an agent has stopped advancing.
62
+ */
63
+ minTurns?: number;
64
+ /** How many times a block must repeat to count as a cycle. Default 3. */
65
+ minRepeats?: number;
66
+ /**
67
+ * Longest cycle to look for, in turns. Default 4.
68
+ *
69
+ * Capped by `window / minRepeats` regardless, since a block cannot repeat
70
+ * three times inside a window that does not hold it three times.
71
+ */
72
+ maxPeriod?: number;
73
+ /**
74
+ * Cycle-coverage threshold. Set null to disable. Default 0.4.
75
+ *
76
+ * Measured on this repo's agent corpus: every healthy trace scores **0.000**,
77
+ * including the traps built to look like loops, and the weakest degenerate
78
+ * trace scores 0.455. The default sits in that gap, nearer the healthy side
79
+ * because nothing healthy approaches it.
80
+ */
81
+ maxAgentLoop?: number | null;
82
+ /**
83
+ * Tools whose calls are dropped before fingerprinting. Empty by default.
84
+ *
85
+ * The escape hatch for a tool whose whole job is to be called repeatedly with
86
+ * identical arguments -- polling a job, sleeping, reading a clock. By shape
87
+ * those are indistinguishable from a loop, so naming them is the only honest
88
+ * way to separate them, in the same way `PROMPT_ECHO` cannot be pointed at a
89
+ * translate endpoint.
90
+ *
91
+ * A turn whose calls are *all* ignored drops out of the trace entirely rather
92
+ * than falling back to its preamble text.
93
+ */
94
+ ignoreTools?: readonly string[];
95
+ }
96
+
97
+ export type { AgentTurn as A, AgentCheckOptions as a, AgentToolCall as b };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The shapes `./agent` reads.
3
+ *
4
+ * Structurally typed and provider-neutral, for the same reason the adapters
5
+ * are: a trace is assembled by the caller from whatever their framework hands
6
+ * back, and requiring a provider's own type here would make this subpath
7
+ * depend on a provider.
8
+ */
9
+ /** One tool call in a turn. */
10
+ interface AgentToolCall {
11
+ /**
12
+ * The tool's name. OpenAI spells it `function.name`, Anthropic and the AI SDK
13
+ * spell it `name`, and the caller maps whichever they have.
14
+ *
15
+ * Optional because a streamed call can arrive before its name does. A call
16
+ * with no name still fingerprints -- by its arguments -- rather than being
17
+ * dropped, since two nameless calls with identical arguments are still the
18
+ * same call twice.
19
+ */
20
+ name?: string | null;
21
+ /**
22
+ * The arguments, in any shape a provider sends: a parsed object, or the raw
23
+ * JSON string OpenAI uses. Both canonicalise to the same fingerprint, so a
24
+ * trace assembled from mixed sources still compares.
25
+ */
26
+ arguments?: unknown;
27
+ }
28
+ /** One model response in an agent run. */
29
+ interface AgentTurn {
30
+ /** The prose the model produced, if any. */
31
+ text?: string | null;
32
+ /**
33
+ * The tool calls the model issued, if any.
34
+ *
35
+ * **A turn carrying tool calls is judged by them alone**, and its `text` is
36
+ * read as a preamble and ignored -- the same rule the single-response
37
+ * adapters already apply. See `internal/turn-fingerprint.ts` for the trap
38
+ * that rule exists to avoid.
39
+ */
40
+ toolCalls?: readonly AgentToolCall[] | null;
41
+ }
42
+ interface AgentCheckOptions {
43
+ /**
44
+ * How many trailing turns to inspect. Default 12.
45
+ *
46
+ * The same idea as `TAIL_LOOP`'s tail: what matters is whether the agent is
47
+ * stuck *now*, not whether it repeated itself twenty turns ago and recovered.
48
+ * A trace shorter than the window is measured whole.
49
+ *
50
+ * It also bounds the longest cycle that can be found, at `window / minRepeats`
51
+ * turns -- 4 at the defaults. Raise both to catch longer orbits, and expect
52
+ * a loop at the very end of a long trace to score lower, because the score is
53
+ * coverage of the window rather than a count.
54
+ */
55
+ window?: number;
56
+ /**
57
+ * Turns required before this will judge at all. Default 4.
58
+ *
59
+ * Below it the detector abstains, which is the rule everywhere else in this
60
+ * package. Two identical turns is a retry, and three is a short poll; neither
61
+ * is evidence that an agent has stopped advancing.
62
+ */
63
+ minTurns?: number;
64
+ /** How many times a block must repeat to count as a cycle. Default 3. */
65
+ minRepeats?: number;
66
+ /**
67
+ * Longest cycle to look for, in turns. Default 4.
68
+ *
69
+ * Capped by `window / minRepeats` regardless, since a block cannot repeat
70
+ * three times inside a window that does not hold it three times.
71
+ */
72
+ maxPeriod?: number;
73
+ /**
74
+ * Cycle-coverage threshold. Set null to disable. Default 0.4.
75
+ *
76
+ * Measured on this repo's agent corpus: every healthy trace scores **0.000**,
77
+ * including the traps built to look like loops, and the weakest degenerate
78
+ * trace scores 0.455. The default sits in that gap, nearer the healthy side
79
+ * because nothing healthy approaches it.
80
+ */
81
+ maxAgentLoop?: number | null;
82
+ /**
83
+ * Tools whose calls are dropped before fingerprinting. Empty by default.
84
+ *
85
+ * The escape hatch for a tool whose whole job is to be called repeatedly with
86
+ * identical arguments -- polling a job, sleeping, reading a clock. By shape
87
+ * those are indistinguishable from a loop, so naming them is the only honest
88
+ * way to separate them, in the same way `PROMPT_ECHO` cannot be pointed at a
89
+ * translate endpoint.
90
+ *
91
+ * A turn whose calls are *all* ignored drops out of the trace entirely rather
92
+ * than falling back to its preamble text.
93
+ */
94
+ ignoreTools?: readonly string[];
95
+ }
96
+
97
+ export type { AgentTurn as A, AgentCheckOptions as a, AgentToolCall as b };
package/dist/agent.d.cts CHANGED
@@ -1,102 +1,8 @@
1
1
  import { V as Verdict } from './types-CxKV_wpA.cjs';
2
+ import { A as AgentTurn, a as AgentCheckOptions } from './agent-types-D310_xck.cjs';
3
+ export { b as AgentToolCall } from './agent-types-D310_xck.cjs';
2
4
  export { D as DegenerateOutputError } from './check-B7lLF8a8.cjs';
3
5
 
4
- /**
5
- * The shapes `./agent` reads.
6
- *
7
- * Structurally typed and provider-neutral, for the same reason the adapters
8
- * are: a trace is assembled by the caller from whatever their framework hands
9
- * back, and requiring a provider's own type here would make this subpath
10
- * depend on a provider.
11
- */
12
- /** One tool call in a turn. */
13
- interface AgentToolCall {
14
- /**
15
- * The tool's name. OpenAI spells it `function.name`, Anthropic and the AI SDK
16
- * spell it `name`, and the caller maps whichever they have.
17
- *
18
- * Optional because a streamed call can arrive before its name does. A call
19
- * with no name still fingerprints -- by its arguments -- rather than being
20
- * dropped, since two nameless calls with identical arguments are still the
21
- * same call twice.
22
- */
23
- name?: string | null;
24
- /**
25
- * The arguments, in any shape a provider sends: a parsed object, or the raw
26
- * JSON string OpenAI uses. Both canonicalise to the same fingerprint, so a
27
- * trace assembled from mixed sources still compares.
28
- */
29
- arguments?: unknown;
30
- }
31
- /** One model response in an agent run. */
32
- interface AgentTurn {
33
- /** The prose the model produced, if any. */
34
- text?: string | null;
35
- /**
36
- * The tool calls the model issued, if any.
37
- *
38
- * **A turn carrying tool calls is judged by them alone**, and its `text` is
39
- * read as a preamble and ignored -- the same rule the single-response
40
- * adapters already apply. See `internal/turn-fingerprint.ts` for the trap
41
- * that rule exists to avoid.
42
- */
43
- toolCalls?: readonly AgentToolCall[] | null;
44
- }
45
- interface AgentCheckOptions {
46
- /**
47
- * How many trailing turns to inspect. Default 12.
48
- *
49
- * The same idea as `TAIL_LOOP`'s tail: what matters is whether the agent is
50
- * stuck *now*, not whether it repeated itself twenty turns ago and recovered.
51
- * A trace shorter than the window is measured whole.
52
- *
53
- * It also bounds the longest cycle that can be found, at `window / minRepeats`
54
- * turns -- 4 at the defaults. Raise both to catch longer orbits, and expect
55
- * a loop at the very end of a long trace to score lower, because the score is
56
- * coverage of the window rather than a count.
57
- */
58
- window?: number;
59
- /**
60
- * Turns required before this will judge at all. Default 4.
61
- *
62
- * Below it the detector abstains, which is the rule everywhere else in this
63
- * package. Two identical turns is a retry, and three is a short poll; neither
64
- * is evidence that an agent has stopped advancing.
65
- */
66
- minTurns?: number;
67
- /** How many times a block must repeat to count as a cycle. Default 3. */
68
- minRepeats?: number;
69
- /**
70
- * Longest cycle to look for, in turns. Default 4.
71
- *
72
- * Capped by `window / minRepeats` regardless, since a block cannot repeat
73
- * three times inside a window that does not hold it three times.
74
- */
75
- maxPeriod?: number;
76
- /**
77
- * Cycle-coverage threshold. Set null to disable. Default 0.4.
78
- *
79
- * Measured on this repo's agent corpus: every healthy trace scores **0.000**,
80
- * including the traps built to look like loops, and the weakest degenerate
81
- * trace scores 0.455. The default sits in that gap, nearer the healthy side
82
- * because nothing healthy approaches it.
83
- */
84
- maxAgentLoop?: number | null;
85
- /**
86
- * Tools whose calls are dropped before fingerprinting. Empty by default.
87
- *
88
- * The escape hatch for a tool whose whole job is to be called repeatedly with
89
- * identical arguments -- polling a job, sleeping, reading a clock. By shape
90
- * those are indistinguishable from a loop, so naming them is the only honest
91
- * way to separate them, in the same way `PROMPT_ECHO` cannot be pointed at a
92
- * translate endpoint.
93
- *
94
- * A turn whose calls are *all* ignored drops out of the trace entirely rather
95
- * than falling back to its preamble text.
96
- */
97
- ignoreTools?: readonly string[];
98
- }
99
-
100
6
  /**
101
7
  * The failure that every other detector in this package scores 0.000 on.
102
8
  *
@@ -238,4 +144,4 @@ interface AgentGuard {
238
144
  */
239
145
  declare function createAgentGuard(options?: AgentCheckOptions): AgentGuard;
240
146
 
241
- export { type AgentCheckOptions, type AgentGuard, type AgentLoopResult, type AgentToolCall, type AgentTurn, agentLoopDetail, agentLoopScore, assertTrace, checkTrace, createAgentGuard };
147
+ export { AgentCheckOptions, type AgentGuard, type AgentLoopResult, AgentTurn, agentLoopDetail, agentLoopScore, assertTrace, checkTrace, createAgentGuard };
package/dist/agent.d.ts CHANGED
@@ -1,102 +1,8 @@
1
1
  import { V as Verdict } from './types-CxKV_wpA.js';
2
+ import { A as AgentTurn, a as AgentCheckOptions } from './agent-types-D310_xck.js';
3
+ export { b as AgentToolCall } from './agent-types-D310_xck.js';
2
4
  export { D as DegenerateOutputError } from './check-DiKuVvZM.js';
3
5
 
4
- /**
5
- * The shapes `./agent` reads.
6
- *
7
- * Structurally typed and provider-neutral, for the same reason the adapters
8
- * are: a trace is assembled by the caller from whatever their framework hands
9
- * back, and requiring a provider's own type here would make this subpath
10
- * depend on a provider.
11
- */
12
- /** One tool call in a turn. */
13
- interface AgentToolCall {
14
- /**
15
- * The tool's name. OpenAI spells it `function.name`, Anthropic and the AI SDK
16
- * spell it `name`, and the caller maps whichever they have.
17
- *
18
- * Optional because a streamed call can arrive before its name does. A call
19
- * with no name still fingerprints -- by its arguments -- rather than being
20
- * dropped, since two nameless calls with identical arguments are still the
21
- * same call twice.
22
- */
23
- name?: string | null;
24
- /**
25
- * The arguments, in any shape a provider sends: a parsed object, or the raw
26
- * JSON string OpenAI uses. Both canonicalise to the same fingerprint, so a
27
- * trace assembled from mixed sources still compares.
28
- */
29
- arguments?: unknown;
30
- }
31
- /** One model response in an agent run. */
32
- interface AgentTurn {
33
- /** The prose the model produced, if any. */
34
- text?: string | null;
35
- /**
36
- * The tool calls the model issued, if any.
37
- *
38
- * **A turn carrying tool calls is judged by them alone**, and its `text` is
39
- * read as a preamble and ignored -- the same rule the single-response
40
- * adapters already apply. See `internal/turn-fingerprint.ts` for the trap
41
- * that rule exists to avoid.
42
- */
43
- toolCalls?: readonly AgentToolCall[] | null;
44
- }
45
- interface AgentCheckOptions {
46
- /**
47
- * How many trailing turns to inspect. Default 12.
48
- *
49
- * The same idea as `TAIL_LOOP`'s tail: what matters is whether the agent is
50
- * stuck *now*, not whether it repeated itself twenty turns ago and recovered.
51
- * A trace shorter than the window is measured whole.
52
- *
53
- * It also bounds the longest cycle that can be found, at `window / minRepeats`
54
- * turns -- 4 at the defaults. Raise both to catch longer orbits, and expect
55
- * a loop at the very end of a long trace to score lower, because the score is
56
- * coverage of the window rather than a count.
57
- */
58
- window?: number;
59
- /**
60
- * Turns required before this will judge at all. Default 4.
61
- *
62
- * Below it the detector abstains, which is the rule everywhere else in this
63
- * package. Two identical turns is a retry, and three is a short poll; neither
64
- * is evidence that an agent has stopped advancing.
65
- */
66
- minTurns?: number;
67
- /** How many times a block must repeat to count as a cycle. Default 3. */
68
- minRepeats?: number;
69
- /**
70
- * Longest cycle to look for, in turns. Default 4.
71
- *
72
- * Capped by `window / minRepeats` regardless, since a block cannot repeat
73
- * three times inside a window that does not hold it three times.
74
- */
75
- maxPeriod?: number;
76
- /**
77
- * Cycle-coverage threshold. Set null to disable. Default 0.4.
78
- *
79
- * Measured on this repo's agent corpus: every healthy trace scores **0.000**,
80
- * including the traps built to look like loops, and the weakest degenerate
81
- * trace scores 0.455. The default sits in that gap, nearer the healthy side
82
- * because nothing healthy approaches it.
83
- */
84
- maxAgentLoop?: number | null;
85
- /**
86
- * Tools whose calls are dropped before fingerprinting. Empty by default.
87
- *
88
- * The escape hatch for a tool whose whole job is to be called repeatedly with
89
- * identical arguments -- polling a job, sleeping, reading a clock. By shape
90
- * those are indistinguishable from a loop, so naming them is the only honest
91
- * way to separate them, in the same way `PROMPT_ECHO` cannot be pointed at a
92
- * translate endpoint.
93
- *
94
- * A turn whose calls are *all* ignored drops out of the trace entirely rather
95
- * than falling back to its preamble text.
96
- */
97
- ignoreTools?: readonly string[];
98
- }
99
-
100
6
  /**
101
7
  * The failure that every other detector in this package scores 0.000 on.
102
8
  *
@@ -238,4 +144,4 @@ interface AgentGuard {
238
144
  */
239
145
  declare function createAgentGuard(options?: AgentCheckOptions): AgentGuard;
240
146
 
241
- export { type AgentCheckOptions, type AgentGuard, type AgentLoopResult, type AgentToolCall, type AgentTurn, agentLoopDetail, agentLoopScore, assertTrace, checkTrace, createAgentGuard };
147
+ export { AgentCheckOptions, type AgentGuard, type AgentLoopResult, AgentTurn, agentLoopDetail, agentLoopScore, assertTrace, checkTrace, createAgentGuard };
package/dist/ai-sdk.cjs CHANGED
@@ -748,6 +748,11 @@ function promptFromMessages(messages) {
748
748
  return parts.join("\n\n");
749
749
  }
750
750
 
751
+ // src/internal/as-array.ts
752
+ function asArray(value) {
753
+ return Array.isArray(value) ? value : [];
754
+ }
755
+
751
756
  // src/ai-sdk.ts
752
757
  var isToolPart = (part) => part.type.startsWith("tool-");
753
758
  function finishReasonOf(value) {
@@ -863,7 +868,28 @@ function outputGuard(options = {}) {
863
868
  }
864
869
  };
865
870
  }
871
+ function toTurn(result) {
872
+ if (result == null || typeof result !== "object") return {};
873
+ const value = result;
874
+ if (Array.isArray(value.content)) {
875
+ return {
876
+ text: value.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(""),
877
+ toolCalls: value.content.filter(isToolPart).map((part) => ({
878
+ name: part.toolName,
879
+ arguments: part.input ?? part.args
880
+ }))
881
+ };
882
+ }
883
+ return {
884
+ text: typeof value.text === "string" ? value.text : "",
885
+ toolCalls: asArray(value.toolCalls).map((call) => ({
886
+ name: call.toolName ?? call.name,
887
+ arguments: call.input ?? call.args
888
+ }))
889
+ };
890
+ }
866
891
 
867
892
  exports.outputGuard = outputGuard;
893
+ exports.toTurn = toTurn;
868
894
  //# sourceMappingURL=ai-sdk.cjs.map
869
895
  //# sourceMappingURL=ai-sdk.cjs.map