@kenkaiiii/gg-agent 5.67.0 → 5.68.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/dist/index.d.cts CHANGED
@@ -1,6 +1,82 @@
1
- import { StreamOptions, Message, Tool, ToolResultContent, ServerToolDefinition, Usage, StopReason, AssistantMessage } from '@kenkaiiii/gg-ai';
1
+ import { Message, StreamOptions, Tool, ToolResultContent, ServerToolDefinition, Usage, StopReason, AssistantMessage } from '@kenkaiiii/gg-ai';
2
2
  import { z } from 'zod';
3
3
 
4
+ /**
5
+ * Stream rules: regex guards matched against a response WHILE it streams.
6
+ *
7
+ * A rule costs zero prompt tokens until the model actually breaks it. On a
8
+ * match the agent loop aborts the in-flight response, discards the partial
9
+ * assistant message (nothing is persisted, no partial tool call runs), appends
10
+ * the rule's reminder as a hidden runtime note, and retries the same step.
11
+ *
12
+ * Matching is incremental: each stream (assistant text, or one tool call's
13
+ * argument JSON) keeps a bounded rolling window, so a delta is scanned together
14
+ * with only the last `windowChars` characters before it — never the whole
15
+ * output. A pattern whose match spans more than the window is not detected.
16
+ *
17
+ * Tool-call arguments arrive as JSON text; they are unescaped incrementally so
18
+ * rules see `\n`, `"`, etc. as the model meant them, not as JSON escapes.
19
+ */
20
+
21
+ /** Which streamed output a rule watches. Thinking is never matched. */
22
+ type StreamRuleScope = "text" | "tool" | "both";
23
+ interface StreamRule {
24
+ /** Stable identifier; also the once-per-run dedupe key. */
25
+ name: string;
26
+ /** Pattern tested against the rolling window. `g`/`y` flags are tolerated. */
27
+ pattern: RegExp;
28
+ scope: StreamRuleScope;
29
+ /** Reminder appended to the context when the rule fires. */
30
+ reminder: string;
31
+ /** Restrict tool-scope matching to these tool names. Unset = every tool. */
32
+ tools?: readonly string[];
33
+ }
34
+ interface StreamRulesConfig {
35
+ rules: readonly StreamRule[];
36
+ /** Rule-triggered retries allowed per agent run (one user turn). Default 3. */
37
+ maxRetries?: number;
38
+ /** Characters of prior output kept per stream for cross-delta matches. Default 1024. */
39
+ windowChars?: number;
40
+ }
41
+ type StreamRuleSource = "text" | "tool";
42
+ interface StreamRuleMatch {
43
+ rules: StreamRule[];
44
+ source: StreamRuleSource;
45
+ toolName?: string;
46
+ }
47
+ declare const DEFAULT_STREAM_RULE_MAX_RETRIES = 3;
48
+ declare const DEFAULT_STREAM_RULE_WINDOW_CHARS = 1024;
49
+ /** Incremental JSON string-escape decoder that tolerates escapes split across chunks. */
50
+ declare class JsonEscapeDecoder {
51
+ private pending;
52
+ push(chunk: string): string;
53
+ }
54
+ /**
55
+ * Per-run rule state. Create one per `agentLoop` run; call `beginAttempt()`
56
+ * before every provider attempt so windows never leak across attempts.
57
+ */
58
+ declare class StreamRuleMonitor {
59
+ readonly maxRetries: number;
60
+ private readonly rules;
61
+ private readonly windowChars;
62
+ private readonly fired;
63
+ private retries;
64
+ private textWindow;
65
+ private readonly toolStreams;
66
+ constructor(config: StreamRulesConfig);
67
+ /** False once the retry cap is spent or every rule has fired — skip all matching. */
68
+ get active(): boolean;
69
+ get retriesUsed(): number;
70
+ beginAttempt(): void;
71
+ checkText(delta: string): StreamRuleMatch | null;
72
+ checkToolArgs(id: string, toolName: string, argsDelta: string): StreamRuleMatch | null;
73
+ /** Consume one retry and mark the rules fired. Returns the retry ordinal (1-based). */
74
+ recordTrigger(rules: readonly StreamRule[]): number;
75
+ private match;
76
+ }
77
+ /** The hidden runtime note appended before the retry. */
78
+ declare function buildStreamRuleReminder(rules: readonly StreamRule[]): Message;
79
+
4
80
  interface StructuredToolResult {
5
81
  content: ToolResultContent;
6
82
  details?: unknown;
@@ -146,7 +222,7 @@ interface AgentTruncatedEvent {
146
222
  }
147
223
  interface AgentRetryEvent {
148
224
  type: "retry";
149
- reason: "overloaded" | "rate_limit" | "provider_error" | "empty_response" | "stream_stall" | "overflow_compact" | "tool_argument_glitch" | "runaway_toolcall";
225
+ reason: "overloaded" | "rate_limit" | "provider_error" | "empty_response" | "stream_stall" | "overflow_compact" | "tool_argument_glitch" | "runaway_toolcall" | "stream_rule";
150
226
  attempt: number;
151
227
  maxAttempts: number;
152
228
  delayMs: number;
@@ -164,6 +240,29 @@ interface AgentRetryEvent {
164
240
  */
165
241
  preservedChars?: number;
166
242
  }
243
+ /**
244
+ * A stream rule matched mid-response. The attempt was aborted and discarded
245
+ * (no partial message persisted, no partial tool call executed), the rule's
246
+ * reminder was appended to the context, and the step is retried. Always
247
+ * followed by a silent `retry` (reason `stream_rule`) so UIs roll back the
248
+ * streamed partial exactly as for any other replayed attempt.
249
+ */
250
+ interface AgentStreamRuleTriggeredEvent {
251
+ type: "stream_rule_triggered";
252
+ /** Names of the rules that matched (usually one). */
253
+ rules: string[];
254
+ source: "text" | "tool";
255
+ /** Tool whose streamed arguments matched, for `source: "tool"`. */
256
+ toolName?: string;
257
+ attempt: number;
258
+ maxAttempts: number;
259
+ /**
260
+ * ESTIMATED usage of the aborted attempt (providers report none for an
261
+ * aborted stream): prompt chars/4 in, streamed chars/4 out. Already added to
262
+ * the run's `totalUsage`.
263
+ */
264
+ usage: Usage;
265
+ }
167
266
  interface AgentToolCallDeltaEvent {
168
267
  type: "toolcall_delta";
169
268
  chars: number;
@@ -192,7 +291,7 @@ interface AgentFollowUpMessageEvent {
192
291
  type: "follow_up_message";
193
292
  content: Message["content"];
194
293
  }
195
- type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentCheckpointEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTurnBudgetExtendedEvent | AgentTruncatedEvent | AgentErrorEvent;
294
+ type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentStreamRuleTriggeredEvent | AgentTurnEndEvent | AgentCheckpointEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTurnBudgetExtendedEvent | AgentTruncatedEvent | AgentErrorEvent;
196
295
  interface TransformContextOptions {
197
296
  /** Force a transform after the provider reports context overflow. */
198
297
  force?: boolean;
@@ -312,6 +411,13 @@ interface AgentOptions {
312
411
  maxTurns: number;
313
412
  extension: number;
314
413
  }) => Promise<boolean> | boolean;
414
+ /**
415
+ * Regex rules matched against streamed assistant text and tool-call
416
+ * arguments. A match aborts the attempt, discards it, appends the rule's
417
+ * reminder and retries the step. Each rule fires at most once per run;
418
+ * `maxRetries` (default 3) caps rule retries per run. Unset = no matching.
419
+ */
420
+ streamRules?: StreamRulesConfig;
315
421
  }
316
422
  interface AgentResult {
317
423
  message: AssistantMessage;
@@ -415,6 +521,7 @@ declare function cancelledBeforeStartText(name: string): string;
415
521
  * something never ran when it did.
416
522
  */
417
523
  declare function indeterminateOutcomeText(name: string): string;
524
+ declare function repairToolPairingAdjacent(messages: Message[]): void;
418
525
 
419
526
  /**
420
527
  * A local inference server (llama.cpp, vLLM, Ollama, LM Studio) can spend
@@ -425,4 +532,4 @@ declare function indeterminateOutcomeText(name: string): string;
425
532
  */
426
533
  declare function isLocalBackendUrl(baseUrl?: string): boolean;
427
534
 
428
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
535
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentStreamRuleTriggeredEvent, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, DEFAULT_STREAM_RULE_MAX_RETRIES, DEFAULT_STREAM_RULE_WINDOW_CHARS, JsonEscapeDecoder, type StreamDiagnosticFn, type StreamRule, type StreamRuleMatch, StreamRuleMonitor, type StreamRuleScope, type StreamRuleSource, type StreamRulesConfig, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, buildStreamRuleReminder, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, repairToolPairingAdjacent, setStreamDiagnostic };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,82 @@
1
- import { StreamOptions, Message, Tool, ToolResultContent, ServerToolDefinition, Usage, StopReason, AssistantMessage } from '@kenkaiiii/gg-ai';
1
+ import { Message, StreamOptions, Tool, ToolResultContent, ServerToolDefinition, Usage, StopReason, AssistantMessage } from '@kenkaiiii/gg-ai';
2
2
  import { z } from 'zod';
3
3
 
4
+ /**
5
+ * Stream rules: regex guards matched against a response WHILE it streams.
6
+ *
7
+ * A rule costs zero prompt tokens until the model actually breaks it. On a
8
+ * match the agent loop aborts the in-flight response, discards the partial
9
+ * assistant message (nothing is persisted, no partial tool call runs), appends
10
+ * the rule's reminder as a hidden runtime note, and retries the same step.
11
+ *
12
+ * Matching is incremental: each stream (assistant text, or one tool call's
13
+ * argument JSON) keeps a bounded rolling window, so a delta is scanned together
14
+ * with only the last `windowChars` characters before it — never the whole
15
+ * output. A pattern whose match spans more than the window is not detected.
16
+ *
17
+ * Tool-call arguments arrive as JSON text; they are unescaped incrementally so
18
+ * rules see `\n`, `"`, etc. as the model meant them, not as JSON escapes.
19
+ */
20
+
21
+ /** Which streamed output a rule watches. Thinking is never matched. */
22
+ type StreamRuleScope = "text" | "tool" | "both";
23
+ interface StreamRule {
24
+ /** Stable identifier; also the once-per-run dedupe key. */
25
+ name: string;
26
+ /** Pattern tested against the rolling window. `g`/`y` flags are tolerated. */
27
+ pattern: RegExp;
28
+ scope: StreamRuleScope;
29
+ /** Reminder appended to the context when the rule fires. */
30
+ reminder: string;
31
+ /** Restrict tool-scope matching to these tool names. Unset = every tool. */
32
+ tools?: readonly string[];
33
+ }
34
+ interface StreamRulesConfig {
35
+ rules: readonly StreamRule[];
36
+ /** Rule-triggered retries allowed per agent run (one user turn). Default 3. */
37
+ maxRetries?: number;
38
+ /** Characters of prior output kept per stream for cross-delta matches. Default 1024. */
39
+ windowChars?: number;
40
+ }
41
+ type StreamRuleSource = "text" | "tool";
42
+ interface StreamRuleMatch {
43
+ rules: StreamRule[];
44
+ source: StreamRuleSource;
45
+ toolName?: string;
46
+ }
47
+ declare const DEFAULT_STREAM_RULE_MAX_RETRIES = 3;
48
+ declare const DEFAULT_STREAM_RULE_WINDOW_CHARS = 1024;
49
+ /** Incremental JSON string-escape decoder that tolerates escapes split across chunks. */
50
+ declare class JsonEscapeDecoder {
51
+ private pending;
52
+ push(chunk: string): string;
53
+ }
54
+ /**
55
+ * Per-run rule state. Create one per `agentLoop` run; call `beginAttempt()`
56
+ * before every provider attempt so windows never leak across attempts.
57
+ */
58
+ declare class StreamRuleMonitor {
59
+ readonly maxRetries: number;
60
+ private readonly rules;
61
+ private readonly windowChars;
62
+ private readonly fired;
63
+ private retries;
64
+ private textWindow;
65
+ private readonly toolStreams;
66
+ constructor(config: StreamRulesConfig);
67
+ /** False once the retry cap is spent or every rule has fired — skip all matching. */
68
+ get active(): boolean;
69
+ get retriesUsed(): number;
70
+ beginAttempt(): void;
71
+ checkText(delta: string): StreamRuleMatch | null;
72
+ checkToolArgs(id: string, toolName: string, argsDelta: string): StreamRuleMatch | null;
73
+ /** Consume one retry and mark the rules fired. Returns the retry ordinal (1-based). */
74
+ recordTrigger(rules: readonly StreamRule[]): number;
75
+ private match;
76
+ }
77
+ /** The hidden runtime note appended before the retry. */
78
+ declare function buildStreamRuleReminder(rules: readonly StreamRule[]): Message;
79
+
4
80
  interface StructuredToolResult {
5
81
  content: ToolResultContent;
6
82
  details?: unknown;
@@ -146,7 +222,7 @@ interface AgentTruncatedEvent {
146
222
  }
147
223
  interface AgentRetryEvent {
148
224
  type: "retry";
149
- reason: "overloaded" | "rate_limit" | "provider_error" | "empty_response" | "stream_stall" | "overflow_compact" | "tool_argument_glitch" | "runaway_toolcall";
225
+ reason: "overloaded" | "rate_limit" | "provider_error" | "empty_response" | "stream_stall" | "overflow_compact" | "tool_argument_glitch" | "runaway_toolcall" | "stream_rule";
150
226
  attempt: number;
151
227
  maxAttempts: number;
152
228
  delayMs: number;
@@ -164,6 +240,29 @@ interface AgentRetryEvent {
164
240
  */
165
241
  preservedChars?: number;
166
242
  }
243
+ /**
244
+ * A stream rule matched mid-response. The attempt was aborted and discarded
245
+ * (no partial message persisted, no partial tool call executed), the rule's
246
+ * reminder was appended to the context, and the step is retried. Always
247
+ * followed by a silent `retry` (reason `stream_rule`) so UIs roll back the
248
+ * streamed partial exactly as for any other replayed attempt.
249
+ */
250
+ interface AgentStreamRuleTriggeredEvent {
251
+ type: "stream_rule_triggered";
252
+ /** Names of the rules that matched (usually one). */
253
+ rules: string[];
254
+ source: "text" | "tool";
255
+ /** Tool whose streamed arguments matched, for `source: "tool"`. */
256
+ toolName?: string;
257
+ attempt: number;
258
+ maxAttempts: number;
259
+ /**
260
+ * ESTIMATED usage of the aborted attempt (providers report none for an
261
+ * aborted stream): prompt chars/4 in, streamed chars/4 out. Already added to
262
+ * the run's `totalUsage`.
263
+ */
264
+ usage: Usage;
265
+ }
167
266
  interface AgentToolCallDeltaEvent {
168
267
  type: "toolcall_delta";
169
268
  chars: number;
@@ -192,7 +291,7 @@ interface AgentFollowUpMessageEvent {
192
291
  type: "follow_up_message";
193
292
  content: Message["content"];
194
293
  }
195
- type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentTurnEndEvent | AgentCheckpointEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTurnBudgetExtendedEvent | AgentTruncatedEvent | AgentErrorEvent;
294
+ type AgentEvent = AgentTextDeltaEvent | AgentThinkingDeltaEvent | AgentToolCallStartEvent | AgentToolCallUpdateEvent | AgentToolCallEndEvent | AgentToolCallDeltaEvent | AgentServerToolCallEvent | AgentServerToolResultEvent | AgentSteeringMessageEvent | AgentFollowUpMessageEvent | AgentRetryEvent | AgentStreamRuleTriggeredEvent | AgentTurnEndEvent | AgentCheckpointEvent | AgentDoneEvent | AgentMaxTurnsEvent | AgentTurnBudgetExtendedEvent | AgentTruncatedEvent | AgentErrorEvent;
196
295
  interface TransformContextOptions {
197
296
  /** Force a transform after the provider reports context overflow. */
198
297
  force?: boolean;
@@ -312,6 +411,13 @@ interface AgentOptions {
312
411
  maxTurns: number;
313
412
  extension: number;
314
413
  }) => Promise<boolean> | boolean;
414
+ /**
415
+ * Regex rules matched against streamed assistant text and tool-call
416
+ * arguments. A match aborts the attempt, discards it, appends the rule's
417
+ * reminder and retries the step. Each rule fires at most once per run;
418
+ * `maxRetries` (default 3) caps rule retries per run. Unset = no matching.
419
+ */
420
+ streamRules?: StreamRulesConfig;
315
421
  }
316
422
  interface AgentResult {
317
423
  message: AssistantMessage;
@@ -415,6 +521,7 @@ declare function cancelledBeforeStartText(name: string): string;
415
521
  * something never ran when it did.
416
522
  */
417
523
  declare function indeterminateOutcomeText(name: string): string;
524
+ declare function repairToolPairingAdjacent(messages: Message[]): void;
418
525
 
419
526
  /**
420
527
  * A local inference server (llama.cpp, vLLM, Ollama, LM Studio) can spend
@@ -425,4 +532,4 @@ declare function indeterminateOutcomeText(name: string): string;
425
532
  */
426
533
  declare function isLocalBackendUrl(baseUrl?: string): boolean;
427
534
 
428
- export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, type StreamDiagnosticFn, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, setStreamDiagnostic };
535
+ export { Agent, type AgentDoneEvent, type AgentErrorEvent, type AgentEvent, type AgentFollowUpMessageEvent, type AgentOptions, type AgentResult, type AgentRetryEvent, type AgentServerToolCallEvent, type AgentServerToolResultEvent, type AgentSteeringMessageEvent, AgentStream, type AgentStreamRuleTriggeredEvent, type AgentTextDeltaEvent, type AgentThinkingDeltaEvent, type AgentTool, type AgentToolCallDeltaEvent, type AgentToolCallEndEvent, type AgentToolCallStartEvent, type AgentToolCallUpdateEvent, type AgentTurnEndEvent, type AgentTurnTiming, DEFAULT_STREAM_RULE_MAX_RETRIES, DEFAULT_STREAM_RULE_WINDOW_CHARS, JsonEscapeDecoder, type StreamDiagnosticFn, type StreamRule, type StreamRuleMatch, StreamRuleMonitor, type StreamRuleScope, type StreamRuleSource, type StreamRulesConfig, type StructuredToolResult, type ToolContext, type ToolExecuteResult, type ToolExecutionMode, type TransformContextOptions, agentLoop, buildStreamRuleReminder, cancelledBeforeStartText, indeterminateOutcomeText, isAbortError, isBillingError, isContextOverflow, isLocalBackendUrl, isUsageLimitError, repairToolPairingAdjacent, setStreamDiagnostic };
package/dist/index.js CHANGED
@@ -90,6 +90,133 @@ function clampOutputTokens(key, requested) {
90
90
  return requested === void 0 ? ceiling : Math.min(requested, ceiling);
91
91
  }
92
92
 
93
+ // src/stream-rules.ts
94
+ var DEFAULT_STREAM_RULE_MAX_RETRIES = 3;
95
+ var DEFAULT_STREAM_RULE_WINDOW_CHARS = 1024;
96
+ var JSON_ESCAPES = {
97
+ n: "\n",
98
+ r: "\r",
99
+ t: " ",
100
+ b: "\b",
101
+ f: "\f",
102
+ '"': '"',
103
+ "\\": "\\",
104
+ "/": "/"
105
+ };
106
+ var JsonEscapeDecoder = class {
107
+ pending = "";
108
+ push(chunk) {
109
+ const input = this.pending + chunk;
110
+ this.pending = "";
111
+ if (!input.includes("\\")) return input;
112
+ let out = "";
113
+ let i = 0;
114
+ while (i < input.length) {
115
+ const ch = input.charAt(i);
116
+ if (ch !== "\\") {
117
+ out += ch;
118
+ i++;
119
+ continue;
120
+ }
121
+ if (i + 1 >= input.length) {
122
+ this.pending = input.slice(i);
123
+ break;
124
+ }
125
+ const next = input.charAt(i + 1);
126
+ if (next === "u") {
127
+ if (i + 6 > input.length) {
128
+ this.pending = input.slice(i);
129
+ break;
130
+ }
131
+ const hex = input.slice(i + 2, i + 6);
132
+ out += /^[0-9a-fA-F]{4}$/.test(hex) ? String.fromCharCode(Number.parseInt(hex, 16)) : input.slice(i, i + 6);
133
+ i += 6;
134
+ continue;
135
+ }
136
+ out += JSON_ESCAPES[next] ?? next;
137
+ i += 2;
138
+ }
139
+ return out;
140
+ }
141
+ };
142
+ var StreamRuleMonitor = class {
143
+ maxRetries;
144
+ rules;
145
+ windowChars;
146
+ fired = /* @__PURE__ */ new Set();
147
+ retries = 0;
148
+ textWindow = "";
149
+ toolStreams = /* @__PURE__ */ new Map();
150
+ constructor(config) {
151
+ this.rules = config.rules;
152
+ this.maxRetries = Math.max(0, config.maxRetries ?? DEFAULT_STREAM_RULE_MAX_RETRIES);
153
+ this.windowChars = Math.max(64, config.windowChars ?? DEFAULT_STREAM_RULE_WINDOW_CHARS);
154
+ }
155
+ /** False once the retry cap is spent or every rule has fired — skip all matching. */
156
+ get active() {
157
+ return this.retries < this.maxRetries && this.fired.size < this.rules.length;
158
+ }
159
+ get retriesUsed() {
160
+ return this.retries;
161
+ }
162
+ beginAttempt() {
163
+ this.textWindow = "";
164
+ this.toolStreams.clear();
165
+ }
166
+ checkText(delta) {
167
+ if (!this.active || !delta) return null;
168
+ const scan = this.textWindow + delta;
169
+ this.textWindow = scan.slice(-this.windowChars);
170
+ const rules = this.match(scan, "text");
171
+ return rules.length > 0 ? { rules, source: "text" } : null;
172
+ }
173
+ checkToolArgs(id, toolName, argsDelta) {
174
+ if (!this.active || !argsDelta) return null;
175
+ let state = this.toolStreams.get(id);
176
+ if (!state) {
177
+ state = { window: "", decoder: new JsonEscapeDecoder() };
178
+ this.toolStreams.set(id, state);
179
+ }
180
+ const scan = state.window + state.decoder.push(argsDelta);
181
+ state.window = scan.slice(-this.windowChars);
182
+ const rules = this.match(scan, "tool", toolName);
183
+ return rules.length > 0 ? { rules, source: "tool", toolName } : null;
184
+ }
185
+ /** Consume one retry and mark the rules fired. Returns the retry ordinal (1-based). */
186
+ recordTrigger(rules) {
187
+ for (const rule of rules) this.fired.add(rule.name);
188
+ this.retries++;
189
+ return this.retries;
190
+ }
191
+ match(scan, source, toolName) {
192
+ const hits = [];
193
+ for (const rule of this.rules) {
194
+ if (this.fired.has(rule.name)) continue;
195
+ if (rule.scope !== "both" && rule.scope !== source) continue;
196
+ if (source === "tool" && rule.tools && toolName !== void 0) {
197
+ if (!rule.tools.includes(toolName)) continue;
198
+ }
199
+ rule.pattern.lastIndex = 0;
200
+ if (rule.pattern.test(scan)) hits.push(rule);
201
+ }
202
+ return hits;
203
+ }
204
+ };
205
+ function buildStreamRuleReminder(rules) {
206
+ const blocks = rules.map(
207
+ (rule) => `<system-reminder reason="stream_rule" rule="${rule.name.replace(/"/g, "'")}">
208
+ Your previous response was interrupted mid-stream because it violated this rule. The interrupted output was discarded: the user never saw it and no tool call from it ran. Regenerate the response from the start and follow the rule:
209
+
210
+ ${rule.reminder.trim()}
211
+ </system-reminder>`
212
+ );
213
+ return {
214
+ role: "user",
215
+ content: blocks.join("\n\n"),
216
+ provenance: { source: "runtime", kind: "notification", visibility: "hidden" }
217
+ };
218
+ }
219
+
93
220
  // src/agent-loop.ts
94
221
  var DEFAULT_MAX_TURNS = 300;
95
222
  var DEFAULT_TOOL_TIMEOUT_MS = 3e5;
@@ -355,6 +482,7 @@ async function* agentLoop(messages, options) {
355
482
  let runawayToolcallRetries = 0;
356
483
  let overflowCompactionAttempts = 0;
357
484
  let toolResultTruncationAttempted = false;
485
+ const streamRuleMonitor = options.streamRules && options.streamRules.rules.length > 0 ? new StreamRuleMonitor(options.streamRules) : null;
358
486
  const invalidToolArgumentCounts = /* @__PURE__ */ new Map();
359
487
  let toolArgumentAutoContinueUsed = false;
360
488
  let useNonStreamingFallback = false;
@@ -408,16 +536,7 @@ async function* agentLoop(messages, options) {
408
536
  turn++;
409
537
  if (logicalTurnStartedAt === 0) logicalTurnStartedAt = Date.now();
410
538
  toolMap = new Map((options.tools ?? []).map((t) => [t.name, t]));
411
- let msgChars = 0;
412
- for (const m of messages) {
413
- if (typeof m.content === "string") msgChars += m.content.length;
414
- else if (Array.isArray(m.content)) {
415
- for (const p of m.content) {
416
- if ("text" in p && typeof p.text === "string") msgChars += p.text.length;
417
- if ("content" in p && typeof p.content === "string") msgChars += p.content.length;
418
- }
419
- }
420
- }
539
+ const msgChars = countMessageChars(messages);
421
540
  let firstEventTimeoutMs;
422
541
  let initialHardTimeoutMs;
423
542
  if (baseFirstEventTimeoutMs === STREAM_FIRST_EVENT_TIMEOUT_MS) {
@@ -487,6 +606,9 @@ async function* agentLoop(messages, options) {
487
606
  let toolcallDeltaCount = 0;
488
607
  let runawayDetected = null;
489
608
  let attemptText = "";
609
+ let streamRuleHit = null;
610
+ let attemptThinkingChars = 0;
611
+ streamRuleMonitor?.beginAttempt();
490
612
  let lastYieldEndTime = Date.now();
491
613
  let maxConsumerLagMs = 0;
492
614
  const forwardAbort = () => streamController.abort();
@@ -645,8 +767,14 @@ async function* agentLoop(messages, options) {
645
767
  }
646
768
  if (event.type === "text_delta") {
647
769
  attemptText += event.text;
770
+ streamRuleHit = streamRuleMonitor?.checkText(event.text) ?? null;
771
+ if (streamRuleHit) {
772
+ streamController.abort();
773
+ break;
774
+ }
648
775
  yield { type: "text_delta", text: event.text };
649
776
  } else if (event.type === "thinking_delta") {
777
+ attemptThinkingChars += event.text.length;
650
778
  yield { type: "thinking_delta", text: event.text };
651
779
  } else if (event.type === "server_toolcall") {
652
780
  yield {
@@ -666,6 +794,11 @@ async function* agentLoop(messages, options) {
666
794
  const chunkChars = event.argsJson?.length ?? 0;
667
795
  toolcallDeltaChars += chunkChars;
668
796
  toolcallDeltaCount++;
797
+ streamRuleHit = streamRuleMonitor?.checkToolArgs(event.id, event.name, event.argsJson ?? "") ?? null;
798
+ if (streamRuleHit) {
799
+ streamController.abort();
800
+ break;
801
+ }
669
802
  if (!runawayDetected && (toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS || toolcallDeltaCount > MAX_TOOLCALL_DELTA_EVENTS)) {
670
803
  runawayDetected = {
671
804
  kind: toolcallDeltaChars > MAX_TOOLCALL_DELTA_CHARS ? "chars" : "events",
@@ -687,6 +820,54 @@ async function* agentLoop(messages, options) {
687
820
  lastYieldEndTime = Date.now();
688
821
  resetIdleTimer();
689
822
  }
823
+ if (streamRuleHit && streamRuleMonitor) {
824
+ const attempt = streamRuleMonitor.recordTrigger(streamRuleHit.rules);
825
+ const ruleNames = streamRuleHit.rules.map((rule) => rule.name);
826
+ const priorPrompt = latestProviderUsage ? latestProviderUsage.inputTokens + (latestProviderUsage.cacheRead ?? 0) + (latestProviderUsage.cacheWrite ?? 0) : 0;
827
+ const newChars = priorPrompt > 0 && usageAnchorIndex !== void 0 ? countMessageChars(messages.slice(usageAnchorIndex + 1)) : msgChars;
828
+ const abortedUsage = {
829
+ inputTokens: Math.ceil(newChars / 4),
830
+ outputTokens: Math.ceil(
831
+ (attemptText.length + attemptThinkingChars + toolcallDeltaChars) / 4
832
+ ),
833
+ ...priorPrompt > 0 ? { cacheRead: priorPrompt } : {}
834
+ };
835
+ totalUsage.inputTokens += abortedUsage.inputTokens;
836
+ totalUsage.outputTokens += abortedUsage.outputTokens;
837
+ if (abortedUsage.cacheRead) {
838
+ totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + abortedUsage.cacheRead;
839
+ }
840
+ diag("stream_rule_triggered", {
841
+ rules: ruleNames.join(","),
842
+ source: streamRuleHit.source,
843
+ toolName: streamRuleHit.toolName,
844
+ attempt,
845
+ maxAttempts: streamRuleMonitor.maxRetries,
846
+ discardedChars: attemptText.length + toolcallDeltaChars,
847
+ provider: options.provider,
848
+ model: options.model
849
+ });
850
+ messages.push(buildStreamRuleReminder(streamRuleHit.rules));
851
+ yield {
852
+ type: "stream_rule_triggered",
853
+ rules: ruleNames,
854
+ source: streamRuleHit.source,
855
+ ...streamRuleHit.toolName !== void 0 ? { toolName: streamRuleHit.toolName } : {},
856
+ attempt,
857
+ maxAttempts: streamRuleMonitor.maxRetries,
858
+ usage: abortedUsage
859
+ };
860
+ yield {
861
+ type: "retry",
862
+ reason: "stream_rule",
863
+ attempt,
864
+ maxAttempts: streamRuleMonitor.maxRetries,
865
+ delayMs: 0,
866
+ silent: true
867
+ };
868
+ turn--;
869
+ continue;
870
+ }
690
871
  diag("stream_done", {
691
872
  events: streamEventCount,
692
873
  totalMs: Date.now() - streamCallStart,
@@ -1733,6 +1914,19 @@ function sanitizeOrphanedServerTools(messages) {
1733
1914
  break;
1734
1915
  }
1735
1916
  }
1917
+ function countMessageChars(messages) {
1918
+ let chars = 0;
1919
+ for (const m of messages) {
1920
+ if (typeof m.content === "string") chars += m.content.length;
1921
+ else if (Array.isArray(m.content)) {
1922
+ for (const p of m.content) {
1923
+ if ("text" in p && typeof p.text === "string") chars += p.text.length;
1924
+ if ("content" in p && typeof p.content === "string") chars += p.content.length;
1925
+ }
1926
+ }
1927
+ }
1928
+ return chars;
1929
+ }
1736
1930
  function repairToolPairingAdjacent(messages) {
1737
1931
  for (let i = 0; i < messages.length; i++) {
1738
1932
  const msg = messages[i];
@@ -1911,7 +2105,12 @@ var Agent = class {
1911
2105
  export {
1912
2106
  Agent,
1913
2107
  AgentStream,
2108
+ DEFAULT_STREAM_RULE_MAX_RETRIES,
2109
+ DEFAULT_STREAM_RULE_WINDOW_CHARS,
2110
+ JsonEscapeDecoder,
2111
+ StreamRuleMonitor,
1914
2112
  agentLoop,
2113
+ buildStreamRuleReminder,
1915
2114
  cancelledBeforeStartText,
1916
2115
  indeterminateOutcomeText,
1917
2116
  isAbortError,
@@ -1919,6 +2118,7 @@ export {
1919
2118
  isContextOverflow,
1920
2119
  isLocalBackendUrl,
1921
2120
  isUsageLimitError,
2121
+ repairToolPairingAdjacent,
1922
2122
  setStreamDiagnostic
1923
2123
  };
1924
2124
  //# sourceMappingURL=index.js.map