@xandreed/plugin-agent-loop 0.13.0 → 0.14.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 +1 -1
- package/dist/loop.d.ts +70 -9
- package/dist/loop.js +107 -86
- package/dist/step-loop.adapter.d.ts +6 -1
- package/dist/step-loop.adapter.js +7 -2
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/loop.d.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface RunLoopOptions<Tools extends Record<string, Tool.Any>, R> {
|
|
|
46
46
|
}, AiError.AiError, R>;
|
|
47
47
|
/** Validate independently reconstructed durable input immediately before a provider dispatch. */
|
|
48
48
|
readonly beforeDispatch?: (step: number, request: ModelDispatch) => Effect.Effect<void, never, R>;
|
|
49
|
+
/**
|
|
50
|
+
* A provider refused a step's request over an image (an `InvalidRequestError`
|
|
51
|
+
* that names one; the argument is what it said). True: the images are
|
|
52
|
+
* dealt with (memory omits them), and the loop renders the step again and
|
|
53
|
+
* sends it once more; false, or absent: the step fails with the refusal.
|
|
54
|
+
*/
|
|
55
|
+
readonly onMediaRejected?: (reason: string) => Effect.Effect<boolean, never, R>;
|
|
49
56
|
readonly system: string | Prompt.Prompt;
|
|
50
57
|
/** The READ side of memory: when present, each provider step sends these
|
|
51
58
|
* messages instead of the loop's own buffer. Persistence still flows through
|
|
@@ -60,8 +67,11 @@ export interface RunLoopOptions<Tools extends Record<string, Tool.Any>, R> {
|
|
|
60
67
|
readonly malformed?: (tools: ReadonlyArray<string>, description: string) => string;
|
|
61
68
|
readonly incomplete?: string;
|
|
62
69
|
};
|
|
63
|
-
/** Explicit opt-in: export prompt and result content on step spans
|
|
64
|
-
*
|
|
70
|
+
/** Explicit opt-in: export prompt and result content on step spans
|
|
71
|
+
* (`engine.step.input`, `engine.step.output`), and to the model calls
|
|
72
|
+
* through `CurrentTraceContent`, which the loop provides for the run.
|
|
73
|
+
* Hosts own consent, redaction and exporter retention. Defaults to
|
|
74
|
+
* `CurrentTraceContent` where the run starts: off unless provided. */
|
|
65
75
|
readonly captureTraceContent?: boolean;
|
|
66
76
|
readonly messages: ReadonlyArray<AgentMessage>;
|
|
67
77
|
readonly toolkit: Toolkit.Toolkit<Tools>;
|
|
@@ -108,7 +118,8 @@ export interface RunLoopOptions<Tools extends Record<string, Tool.Any>, R> {
|
|
|
108
118
|
* settled turn shape while fanning `assistant_delta` events as tokens
|
|
109
119
|
* flow. Default FALSE. A stream that fails before its first part
|
|
110
120
|
* (scripted providers' `Stream.die` included) falls back to
|
|
111
|
-
* `generateText`
|
|
121
|
+
* `generateText` — for the REST of the run, not per turn — and emits one
|
|
122
|
+
* `stream_fallback` event with the stream's error.
|
|
112
123
|
*/
|
|
113
124
|
readonly streaming?: boolean;
|
|
114
125
|
/**
|
|
@@ -164,7 +175,19 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
|
|
|
164
175
|
finalText: string;
|
|
165
176
|
messages: readonly ({
|
|
166
177
|
readonly role: "user";
|
|
167
|
-
readonly content: string
|
|
178
|
+
readonly content: string | readonly ({
|
|
179
|
+
readonly type: "media";
|
|
180
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
181
|
+
readonly data: string;
|
|
182
|
+
readonly fingerprint: string & import("effect/Brand").Brand<"MediaFingerprint">;
|
|
183
|
+
readonly bytes: number;
|
|
184
|
+
readonly name?: string;
|
|
185
|
+
readonly alt?: string;
|
|
186
|
+
} | {
|
|
187
|
+
readonly type: "text";
|
|
188
|
+
readonly text: string;
|
|
189
|
+
readonly providerOptions?: unknown;
|
|
190
|
+
})[];
|
|
168
191
|
readonly providerOptions?: unknown;
|
|
169
192
|
} | {
|
|
170
193
|
readonly role: "assistant";
|
|
@@ -178,9 +201,9 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
|
|
|
178
201
|
readonly providerOptions?: unknown;
|
|
179
202
|
} | {
|
|
180
203
|
readonly type: "tool-call";
|
|
204
|
+
readonly input: unknown;
|
|
181
205
|
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
182
206
|
readonly toolName: string;
|
|
183
|
-
readonly input: unknown;
|
|
184
207
|
readonly providerOptions?: unknown;
|
|
185
208
|
readonly providerExecuted?: boolean | undefined;
|
|
186
209
|
})[];
|
|
@@ -189,9 +212,18 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
|
|
|
189
212
|
readonly role: "tool";
|
|
190
213
|
readonly content: readonly {
|
|
191
214
|
readonly type: "tool-result";
|
|
215
|
+
readonly output: unknown;
|
|
192
216
|
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
193
217
|
readonly toolName: string;
|
|
194
|
-
readonly
|
|
218
|
+
readonly media?: readonly {
|
|
219
|
+
readonly type: "media";
|
|
220
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
221
|
+
readonly data: string;
|
|
222
|
+
readonly fingerprint: string & import("effect/Brand").Brand<"MediaFingerprint">;
|
|
223
|
+
readonly bytes: number;
|
|
224
|
+
readonly name?: string;
|
|
225
|
+
readonly alt?: string;
|
|
226
|
+
}[];
|
|
195
227
|
readonly providerOptions?: unknown;
|
|
196
228
|
readonly isError?: boolean | undefined;
|
|
197
229
|
}[];
|
|
@@ -199,7 +231,19 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
|
|
|
199
231
|
})[];
|
|
200
232
|
newTail: readonly ({
|
|
201
233
|
readonly role: "user";
|
|
202
|
-
readonly content: string
|
|
234
|
+
readonly content: string | readonly ({
|
|
235
|
+
readonly type: "media";
|
|
236
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
237
|
+
readonly data: string;
|
|
238
|
+
readonly fingerprint: string & import("effect/Brand").Brand<"MediaFingerprint">;
|
|
239
|
+
readonly bytes: number;
|
|
240
|
+
readonly name?: string;
|
|
241
|
+
readonly alt?: string;
|
|
242
|
+
} | {
|
|
243
|
+
readonly type: "text";
|
|
244
|
+
readonly text: string;
|
|
245
|
+
readonly providerOptions?: unknown;
|
|
246
|
+
})[];
|
|
203
247
|
readonly providerOptions?: unknown;
|
|
204
248
|
} | {
|
|
205
249
|
readonly role: "assistant";
|
|
@@ -213,9 +257,9 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
|
|
|
213
257
|
readonly providerOptions?: unknown;
|
|
214
258
|
} | {
|
|
215
259
|
readonly type: "tool-call";
|
|
260
|
+
readonly input: unknown;
|
|
216
261
|
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
217
262
|
readonly toolName: string;
|
|
218
|
-
readonly input: unknown;
|
|
219
263
|
readonly providerOptions?: unknown;
|
|
220
264
|
readonly providerExecuted?: boolean | undefined;
|
|
221
265
|
})[];
|
|
@@ -224,9 +268,18 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
|
|
|
224
268
|
readonly role: "tool";
|
|
225
269
|
readonly content: readonly {
|
|
226
270
|
readonly type: "tool-result";
|
|
271
|
+
readonly output: unknown;
|
|
227
272
|
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
228
273
|
readonly toolName: string;
|
|
229
|
-
readonly
|
|
274
|
+
readonly media?: readonly {
|
|
275
|
+
readonly type: "media";
|
|
276
|
+
readonly mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
277
|
+
readonly data: string;
|
|
278
|
+
readonly fingerprint: string & import("effect/Brand").Brand<"MediaFingerprint">;
|
|
279
|
+
readonly bytes: number;
|
|
280
|
+
readonly name?: string;
|
|
281
|
+
readonly alt?: string;
|
|
282
|
+
}[];
|
|
230
283
|
readonly providerOptions?: unknown;
|
|
231
284
|
readonly isError?: boolean | undefined;
|
|
232
285
|
}[];
|
|
@@ -234,4 +287,12 @@ export declare const runLoop: <Tools extends Record<string, Tool.Any>, R = never
|
|
|
234
287
|
})[];
|
|
235
288
|
outcome: "ok" | "partial";
|
|
236
289
|
reason: "completed" | "step-cap" | "degenerate-loop";
|
|
290
|
+
usage: {
|
|
291
|
+
readonly inputTokens: number;
|
|
292
|
+
readonly outputTokens: number;
|
|
293
|
+
readonly totalTokens: number;
|
|
294
|
+
readonly cacheReadTokens: number;
|
|
295
|
+
readonly cacheWriteTokens: number;
|
|
296
|
+
readonly reasoningTokens: number;
|
|
297
|
+
};
|
|
237
298
|
}, AiError.AiError, LanguageModel.LanguageModel | Exclude<R, import("effect/Tracer").ParentSpan> | Exclude<Tool.HandlersFor<Tools>, import("effect/Tracer").ParentSpan>>;
|
package/dist/loop.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AiError, LanguageModel, Prompt } from "effect/ai";
|
|
2
|
-
import { Cause, Effect, Exit, Match, Metric, Option, Ref, Stream } from "effect";
|
|
3
|
-
import { foldStreamParts, freezeModelRequest } from "@xandreed/core";
|
|
2
|
+
import { Cause, Effect, Exit, Formatter, Match, Metric, Option, Ref, Result, Stream } from "effect";
|
|
3
|
+
import { CurrentTraceContent, foldStreamParts, freezeModelRequest, replayableContext } from "@xandreed/core";
|
|
4
4
|
import { CurrentAgentStep } from "@xandreed/core";
|
|
5
5
|
import { CurrentEmptyResponseTolerance } from "@xandreed/core";
|
|
6
|
-
import { addUsage, zeroUsage } from "@xandreed/core";
|
|
6
|
+
import { addUsage, elideMediaData, isMediaValue, zeroUsage } from "@xandreed/core";
|
|
7
7
|
import { extractModel, extractUsage, handoffToMessage, responseReasoning, responseText, responseToAgentMessages, responseToolCalls, responseToolResults, toPromptMessages, withToolCallIds, withUsageOnAssistant } from "@xandreed/core";
|
|
8
8
|
export const DEFAULT_MAX_STEPS = 100, DEFAULT_TOOL_CONCURRENCY = 4;
|
|
9
9
|
const engineRuns = Metric.counter("engine.runs", {
|
|
@@ -13,11 +13,24 @@ const engineRuns = Metric.counter("engine.runs", {
|
|
|
13
13
|
description: "tool calls by tool name and result",
|
|
14
14
|
incremental: !0
|
|
15
15
|
}), engineCorrections = Metric.counter("engine.corrections", {
|
|
16
|
-
description: "self-corrections by kind (malformed recovery, degenerate nudge)",
|
|
16
|
+
description: "self-corrections by kind (malformed recovery, degenerate nudge, stream fallback, image refused and resent)",
|
|
17
17
|
incremental: !0
|
|
18
|
-
}), tagged = (metric, tags) => Metric.withAttributes(metric, tags),
|
|
18
|
+
}), tagged = (metric, tags) => Metric.withAttributes(metric, tags), stepUsageAttributes = (usage) => ({
|
|
19
|
+
"gen_ai.usage.input_tokens": usage.inputTokens,
|
|
20
|
+
"gen_ai.usage.output_tokens": usage.outputTokens,
|
|
21
|
+
"gen_ai.usage.cache_read.input_tokens": usage.cacheReadTokens,
|
|
22
|
+
"gen_ai.usage.cache_creation.input_tokens": usage.cacheWriteTokens,
|
|
23
|
+
"gen_ai.usage.reasoning.output_tokens": usage.reasoningTokens
|
|
24
|
+
}), runUsageAttributes = (usage) => ({
|
|
25
|
+
"engine.usage.input_tokens": usage.inputTokens,
|
|
26
|
+
"engine.usage.output_tokens": usage.outputTokens,
|
|
27
|
+
"engine.usage.cache_read_tokens": usage.cacheReadTokens,
|
|
28
|
+
"engine.usage.cache_write_tokens": usage.cacheWriteTokens,
|
|
29
|
+
"engine.usage.reasoning_tokens": usage.reasoningTokens,
|
|
30
|
+
"engine.usage.total_tokens": usage.totalTokens
|
|
31
|
+
}), malformedReasons = new Set(["InvalidOutputError", "ToolParameterValidationError", "ToolNotFoundError"]), isMalformed = (error) => AiError.isAiError(error) && malformedReasons.has(error.reason._tag), isImageRejection = (error) => AiError.isAiError(error) && error.reason._tag === "InvalidRequestError" && /image/i.test(error.reason.message), MAX_MALFORMED = 3, REPEAT_NUDGE_AT = 3, REPEAT_BREAK_AT = 5;
|
|
19
32
|
export const DEGENERATE_REPEAT_NUDGE = "You've called the same tool(s) and gotten the same result several times in " + "a row \u2014 you're looping without making progress. STOP repeating that call. " + "Take a DIFFERENT concrete action toward the task; if you're genuinely stuck, say what's blocking you and stop.", DEGENERATE_LOOP_STOP = "[stopped: repeated the same tool call with no progress \u2014 the run was looping]";
|
|
20
|
-
const clip = (s, max) => s.length <= max ? s : `${s.slice(0, max)}\u2026`, progressSignature = (content, pollable) => {
|
|
33
|
+
const clip = (s, max) => s.length <= max ? s : `${s.slice(0, max)}\u2026`, FALLBACK_MESSAGE_CHARS = 300, failureText = (failure) => typeof failure === "string" ? failure : Formatter.format(failure), progressSignature = (content, pollable) => {
|
|
21
34
|
const calls = responseToolCalls(content);
|
|
22
35
|
if (calls.length === 0)
|
|
23
36
|
return "";
|
|
@@ -33,86 +46,93 @@ export const foldProgress = (state, sig) => {
|
|
|
33
46
|
nudge: sig !== "" && stale === REPEAT_NUDGE_AT,
|
|
34
47
|
broke: sig !== "" && stale >= REPEAT_BREAK_AT
|
|
35
48
|
};
|
|
36
|
-
}, nextPhase = (facts) => facts.broke ? "degenerate-loop" : !facts.wantsMore ? "completed" : facts.turnIndex >= facts.maxSteps ? "step-cap" : "continue"
|
|
49
|
+
}, nextPhase = (facts) => facts.broke ? "degenerate-loop" : !facts.wantsMore ? "completed" : facts.turnIndex >= facts.maxSteps ? "step-cap" : "continue";
|
|
50
|
+
const tracedJson = (value) => JSON.stringify(value, (_key, field) => isMediaValue(field) ? elideMediaData(field) : typeof field === "object" && field !== null && ("type" in field) && field.type === "file" && ("data" in field) && typeof field.data === "string" ? { ...field, data: `[${field.data.length} base64 characters]` } : field);
|
|
51
|
+
export const runLoop = (options) => Effect.gen(function* () {
|
|
37
52
|
yield* Effect.annotateCurrentSpan({ "engine.input.messages": options.messages.length });
|
|
38
|
-
const maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS, pollable = new Set(options.pollableTools ?? []), eventContext = yield* Effect.context(), onEvent = (event) => Effect.gen(function* () {
|
|
39
|
-
const parent = yield* Effect.currentSpan.pipe(Effect.orDie);
|
|
40
|
-
yield* (options.onEvent?.(event) ?? Effect.void).pipe(Effect.withParentSpan(parent), Effect.provide(eventContext));
|
|
41
|
-
}), toolkit = yield* options.toolkit, step = (state) => Effect.gen(function* () {
|
|
53
|
+
const maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS, pollable = new Set(options.pollableTools ?? []), captureTraceContent = options.captureTraceContent ?? (yield* Effect.service(CurrentTraceContent)), eventContext = replayableContext(yield* Effect.context()), onEvent = (event) => (options.onEvent?.(event) ?? Effect.void).pipe(Effect.provide(eventContext)), toolkit = yield* options.toolkit, step = (state) => Effect.gen(function* () {
|
|
42
54
|
yield* onEvent({ type: "turn_start", turnIndex: state.turnIndex });
|
|
43
55
|
const active = options.activeTools === void 0 ? Object.keys(toolkit.tools) : (yield* options.activeTools().pipe(Effect.provide(eventContext))).map(String), selectedTools = Object.fromEntries(active.flatMap((name) => Object.hasOwn(toolkit.tools, name) ? [[name, toolkit.tools[name]]] : [])), toolNames = Object.keys(selectedTools), planned = state.turnIndex === 0 && options.initialStep !== void 0, stepView = { stepIndex: state.turnIndex, activeTools: toolNames, lastUsage: state.lastUsage };
|
|
44
56
|
yield* Effect.annotateCurrentSpan({ "engine.tools.active": toolNames });
|
|
45
|
-
const prepared = state.turnIndex === 0 && options.initialStep !== void 0 ? Option.none() : options.prepareModel === void 0 ? Option.none() : Option.some(yield* options.prepareModel({ stepIndex: state.turnIndex, messages: state.messages, activeTools: toolNames }).pipe(Effect.provide(eventContext))), baseSystem = Option.match(prepared, { onNone: () => options.system, onSome: (value) => value.system }), instructions = typeof baseSystem === "string" ? Prompt.make([{ role: "system", content: baseSystem }]) : baseSystem,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
content: res.content,
|
|
79
|
-
finishReason: res.finishReason,
|
|
80
|
-
usage: res.usage,
|
|
81
|
-
streamingHealthy
|
|
82
|
-
}))), streamed = Effect.gen(function* () {
|
|
83
|
-
yield* beforeDispatch;
|
|
84
|
-
const partSeen = yield* Ref.make(!1), folded = yield* foldStreamParts(Stream.unwrap(Effect.sync(() => dispatchModel.streamText(callOptions))).pipe(Stream.tap(() => Ref.set(partSeen, !0))), (delta) => onEvent({
|
|
85
|
-
type: "assistant_delta",
|
|
86
|
-
turnIndex: state.turnIndex,
|
|
87
|
-
channel: delta.channel,
|
|
88
|
-
id: delta.id,
|
|
89
|
-
delta: delta.delta,
|
|
90
|
-
...delta.toolName === void 0 ? {} : { toolName: delta.toolName }
|
|
91
|
-
})).pipe(Effect.map((turn) => Option.some({
|
|
57
|
+
const prepared = state.turnIndex === 0 && options.initialStep !== void 0 ? Option.none() : options.prepareModel === void 0 ? Option.none() : Option.some(yield* options.prepareModel({ stepIndex: state.turnIndex, messages: state.messages, activeTools: toolNames }).pipe(Effect.provide(eventContext))), baseSystem = Option.match(prepared, { onNone: () => options.system, onSome: (value) => value.system }), instructions = typeof baseSystem === "string" ? Prompt.make([{ role: "system", content: baseSystem }]) : baseSystem, directive = options.stepDirective === void 0 ? Option.none() : (yield* options.stepDirective(stepView).pipe(Effect.provide(eventContext))).toolChoice, partSeen = yield* Ref.make(!1), attempt = Effect.gen(function* () {
|
|
58
|
+
const visible = options.render === void 0 ? state.messages : yield* options.render(stepView).pipe(Effect.provide(eventContext)), prompt = freezeModelRequest(Prompt.concat(instructions, Prompt.make(toPromptMessages(visible))));
|
|
59
|
+
if (captureTraceContent)
|
|
60
|
+
yield* Effect.annotateCurrentSpan({
|
|
61
|
+
"engine.step.input": tracedJson({ messages: prompt.content, activeTools: toolNames })
|
|
62
|
+
});
|
|
63
|
+
const toolSequence = yield* Ref.make(0), callOptions = Object.freeze({
|
|
64
|
+
prompt,
|
|
65
|
+
toolkit: {
|
|
66
|
+
tools: selectedTools,
|
|
67
|
+
handle: (name, params, providerCallId, parseOptions) => Effect.gen(function* () {
|
|
68
|
+
const sequence = yield* Ref.getAndUpdate(toolSequence, (value) => value + 1), toolCallId = `execution:${state.turnIndex}:${sequence}`;
|
|
69
|
+
yield* onEvent({ type: "tool_start", turnIndex: state.turnIndex, toolCallId, toolName: String(name), args: params });
|
|
70
|
+
return (yield* toolkit.handle(name, params, providerCallId, parseOptions)).pipe(Stream.mapEffect((result) => result.isFailure && result.failureOrigin === "parameters" ? Effect.fail(result.result) : Effect.succeed(result)), Stream.tap((result) => result.preliminary ? Effect.void : onEvent({
|
|
71
|
+
type: "tool_end",
|
|
72
|
+
turnIndex: state.turnIndex,
|
|
73
|
+
toolCallId,
|
|
74
|
+
toolName: String(name),
|
|
75
|
+
args: params,
|
|
76
|
+
ok: !result.isFailure,
|
|
77
|
+
result: result.result,
|
|
78
|
+
encoded: result.encodedResult
|
|
79
|
+
})));
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
concurrency: options.toolConcurrency ?? DEFAULT_TOOL_CONCURRENCY,
|
|
83
|
+
...Option.match(directive, { onNone: () => ({}), onSome: (toolChoice) => ({ toolChoice }) })
|
|
84
|
+
}), dispatchModel = Option.isSome(prepared) ? prepared.value.model : yield* LanguageModel.LanguageModel, beforeDispatch = options.beforeDispatch === void 0 ? Effect.void : options.beforeDispatch(state.turnIndex, {
|
|
85
|
+
prompt,
|
|
86
|
+
tools: Object.values(selectedTools),
|
|
87
|
+
toolChoice: Option.getOrElse(directive, () => "auto"),
|
|
88
|
+
model: dispatchModel
|
|
89
|
+
}).pipe(Effect.provide(eventContext)), generated = planned ? LanguageModel.generateText(callOptions) : beforeDispatch.pipe(Effect.andThen(Effect.suspend(() => dispatchModel.generateText(callOptions)))), settled = (streamingHealthy) => generated.pipe(Effect.map((res) => ({
|
|
92
90
|
_tag: "ok",
|
|
93
|
-
content:
|
|
94
|
-
finishReason:
|
|
95
|
-
usage:
|
|
96
|
-
streamingHealthy
|
|
97
|
-
}))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
91
|
+
content: res.content,
|
|
92
|
+
finishReason: res.finishReason,
|
|
93
|
+
usage: res.usage,
|
|
94
|
+
streamingHealthy
|
|
95
|
+
}))), streamed = Effect.gen(function* () {
|
|
96
|
+
yield* beforeDispatch;
|
|
97
|
+
const folded = yield* foldStreamParts(Stream.unwrap(Effect.sync(() => dispatchModel.streamText(callOptions))).pipe(Stream.tap(() => Ref.set(partSeen, !0))), (delta) => onEvent({
|
|
98
|
+
type: "assistant_delta",
|
|
99
|
+
turnIndex: state.turnIndex,
|
|
100
|
+
channel: delta.channel,
|
|
101
|
+
id: delta.id,
|
|
102
|
+
delta: delta.delta,
|
|
103
|
+
...delta.toolName === void 0 ? {} : { toolName: delta.toolName }
|
|
104
|
+
})).pipe(Effect.map((turn) => Result.succeed({
|
|
105
|
+
_tag: "ok",
|
|
106
|
+
content: turn.content,
|
|
107
|
+
finishReason: turn.finishReason,
|
|
108
|
+
usage: turn.usage,
|
|
109
|
+
streamingHealthy: !0
|
|
110
|
+
})), Effect.catch((err) => Ref.get(partSeen).pipe(Effect.flatMap((armed) => armed || isImageRejection(err) ? Effect.fail(err) : Effect.succeed(Result.fail(failureText(err)))))), Effect.catchDefect((defect) => Ref.get(partSeen).pipe(Effect.flatMap((armed) => armed ? Effect.die(defect) : Effect.succeed(Result.fail(failureText(defect)))))));
|
|
111
|
+
return yield* Result.match(folded, {
|
|
112
|
+
onSuccess: Effect.succeed,
|
|
113
|
+
onFailure: (failure) => Effect.logWarning("streaming failed before any part arrived \u2014 falling back to generateText for this run").pipe(Effect.andThen(onEvent({ type: "stream_fallback", turnIndex: state.turnIndex, message: failure.slice(0, FALLBACK_MESSAGE_CHARS) })), Effect.andThen(Metric.update(tagged(engineCorrections, { "engine.kind": "stream-fallback" }), 1)), Effect.andThen(settled(!1)))
|
|
114
|
+
});
|
|
115
|
+
}), initial = Effect.gen(function* () {
|
|
116
|
+
const provider = yield* LanguageModel.make({
|
|
117
|
+
generateText: () => Effect.succeed([...(options.initialStep ?? []).map((call, index) => ({
|
|
118
|
+
type: "tool-call",
|
|
119
|
+
id: `planned:0:${index}`,
|
|
120
|
+
name: call.name,
|
|
121
|
+
params: call.params,
|
|
122
|
+
providerExecuted: !1
|
|
123
|
+
})), { type: "finish", reason: "tool-calls", usage: { inputTokens: {}, outputTokens: {} } }]),
|
|
124
|
+
streamText: () => Stream.empty
|
|
125
|
+
});
|
|
126
|
+
yield* Effect.annotateCurrentSpan("engine.step.host_planned", !0);
|
|
127
|
+
return yield* settled(state.streamingHealthy).pipe(Effect.provideService(LanguageModel.LanguageModel, provider));
|
|
128
|
+
}), useStreaming = options.streaming === !0 && state.streamingHealthy;
|
|
129
|
+
return yield* planned ? initial : useStreaming ? streamed : settled(state.streamingHealthy);
|
|
130
|
+
}), outcome = yield* attempt.pipe(Effect.catch((err) => Effect.gen(function* () {
|
|
131
|
+
const onMediaRejected = options.onMediaRejected, armed = yield* Ref.get(partSeen);
|
|
132
|
+
if (planned || armed || onMediaRejected === void 0 || !isImageRejection(err))
|
|
133
|
+
return yield* Effect.fail(err);
|
|
134
|
+
return yield* onMediaRejected(clip(err.reason.message, 600)).pipe(Effect.provide(eventContext), Effect.flatMap((retry) => retry ? Effect.logWarning(`the provider refused an image; sending the step again with its images as labels: ${clip(err.reason.message, 300)}`).pipe(Effect.andThen(Metric.update(tagged(engineCorrections, { "engine.kind": "media-rejected" }), 1)), Effect.andThen(attempt)) : Effect.fail(err)));
|
|
135
|
+
})), Effect.catch((err) => isMalformed(err) ? Effect.succeed({ _tag: "malformed", err }) : Effect.fail(err)), Effect.provideService(CurrentEmptyResponseTolerance, state.toolCalls > 0));
|
|
116
136
|
if (outcome._tag === "malformed") {
|
|
117
137
|
const streak = state.malformedStreak + 1;
|
|
118
138
|
if (streak >= MAX_MALFORMED)
|
|
@@ -143,8 +163,8 @@ This usually means you called a tool that doesn't exist or used the wrong argume
|
|
|
143
163
|
yield* Effect.annotateCurrentSpan({
|
|
144
164
|
"engine.step.finish_reason": outcome.finishReason,
|
|
145
165
|
"engine.step.usage_available": !planned,
|
|
146
|
-
...!planned ? { "engine.step.usage": JSON.stringify(usage) } : {},
|
|
147
|
-
...
|
|
166
|
+
...!planned ? { "engine.step.usage": JSON.stringify(usage), ...stepUsageAttributes(usage) } : {},
|
|
167
|
+
...captureTraceContent ? { "engine.step.output": tracedJson({ content, usage, finishReason: outcome.finishReason }) } : {}
|
|
148
168
|
});
|
|
149
169
|
const model = extractModel(content), tail = planned ? responseToAgentMessages(content) : withUsageOnAssistant(responseToAgentMessages(content), usage, model), positions = yield* options.onTail?.(tail) ?? Effect.succeed([]), assistantAt = tail.findIndex((m) => m.role === "assistant"), assistantPosition = assistantAt >= 0 ? Option.fromNullishOr(positions[assistantAt]) : Option.none(), text = responseText(content), toolCalls = responseToolCalls(content);
|
|
150
170
|
yield* onEvent({
|
|
@@ -226,7 +246,7 @@ This usually means you called a tool that doesn't exist or used the wrong argume
|
|
|
226
246
|
toolFailures: 0,
|
|
227
247
|
corrections: 0,
|
|
228
248
|
streamingHealthy: !0
|
|
229
|
-
}, final = yield* runSteps(initial), { outcome, reason } = Match.value(final.phase).pipe(Match.when("completed", () => ({ outcome: "ok", reason: "completed" })), Match.when("step-cap", () => ({
|
|
249
|
+
}, final = yield* runSteps(initial).pipe(Effect.provideService(CurrentTraceContent, captureTraceContent)), { outcome, reason } = Match.value(final.phase).pipe(Match.when("completed", () => ({ outcome: "ok", reason: "completed" })), Match.when("step-cap", () => ({
|
|
230
250
|
outcome: "partial",
|
|
231
251
|
reason: "step-cap"
|
|
232
252
|
})), Match.when("degenerate-loop", () => ({
|
|
@@ -239,7 +259,7 @@ This usually means you called a tool that doesn't exist or used the wrong argume
|
|
|
239
259
|
"engine.outcome": outcome,
|
|
240
260
|
"engine.reason": reason,
|
|
241
261
|
"engine.turns": final.turnIndex,
|
|
242
|
-
|
|
262
|
+
...runUsageAttributes(final.usage),
|
|
243
263
|
"engine.tool_calls": final.toolCalls,
|
|
244
264
|
"engine.tool_calls.failed": final.toolFailures,
|
|
245
265
|
"engine.corrections": final.corrections,
|
|
@@ -250,6 +270,7 @@ This usually means you called a tool that doesn't exist or used the wrong argume
|
|
|
250
270
|
messages: final.messages,
|
|
251
271
|
newTail: final.newTail,
|
|
252
272
|
outcome,
|
|
253
|
-
reason
|
|
273
|
+
reason,
|
|
274
|
+
usage: final.usage
|
|
254
275
|
};
|
|
255
276
|
}).pipe(Effect.withSpan("engine.run"));
|
|
@@ -12,7 +12,12 @@ export declare const STEP_LOOP: {
|
|
|
12
12
|
* evaluates the host's completion (`completion.evaluated`). A verdict that
|
|
13
13
|
* awaits tasks joins them and is evaluated once more. The plan for a step is
|
|
14
14
|
* computed once and serves the model, the messages and the tool choice; a
|
|
15
|
-
* host-planned first batch runs without a provider call or a plan.
|
|
15
|
+
* host-planned first batch runs without a provider call or a plan. A stream
|
|
16
|
+
* that fails before its first part publishes `stream.fallback` once, and the
|
|
17
|
+
* run goes on without streaming. The run's `captureTraceContent` is
|
|
18
|
+
* `CurrentTraceContent` for its model calls. A request a provider refuses
|
|
19
|
+
* over an image goes to `request.onMediaRejected`; when it records an
|
|
20
|
+
* `OmitMedia`, the step is planned again and sent once more.
|
|
16
21
|
*/
|
|
17
22
|
export declare const runSteps: (request: StepRequest) => Effect.Effect<RunResult, HarnessError>;
|
|
18
23
|
export declare const StepLoopLive: Layer.Layer<StepLoop, never, never>;
|
|
@@ -40,6 +40,8 @@ export const runSteps = (request) => Effect.gen(function* () {
|
|
|
40
40
|
usage: event.usage
|
|
41
41
|
});
|
|
42
42
|
}).pipe(Effect.orDie);
|
|
43
|
+
if (event.type === "stream_fallback")
|
|
44
|
+
return request.events.publish({ _tag: "stream.fallback", step: event.turnIndex, message: event.message }).pipe(Effect.orDie);
|
|
43
45
|
if (event.type === "turn_end") {
|
|
44
46
|
const ending = endStep(event.turnIndex, event.status);
|
|
45
47
|
return event.status === "completed" ? ending.pipe(Effect.orDie) : ending.pipe(Effect.catchCause(() => Effect.void));
|
|
@@ -53,6 +55,7 @@ export const runSteps = (request) => Effect.gen(function* () {
|
|
|
53
55
|
toolConcurrency: request.limits.toolConcurrency,
|
|
54
56
|
streaming: request.limits.streaming,
|
|
55
57
|
requireCompletion: request.limits.requireCompletion,
|
|
58
|
+
captureTraceContent: request.captureTraceContent,
|
|
56
59
|
pollableTools: request.tools.pollable,
|
|
57
60
|
...Option.match(request.correctives, { onNone: () => ({}), onSome: (correctives) => ({ correctives }) }),
|
|
58
61
|
...Option.match(request.initial, { onNone: () => ({}), onSome: (batch) => ({ initialStep: batch.calls }) }),
|
|
@@ -66,6 +69,7 @@ export const runSteps = (request) => Effect.gen(function* () {
|
|
|
66
69
|
render: (view) => isPlanned(view.stepIndex) ? Effect.succeed([]) : planFor(view).pipe(Effect.map((plan) => plan.messages), Effect.orDie),
|
|
67
70
|
stepDirective: (view) => isPlanned(view.stepIndex) ? Effect.succeed({ toolChoice: Option.none() }) : planFor(view).pipe(Effect.map((plan) => ({ toolChoice: plan.toolChoice })), Effect.orDie),
|
|
68
71
|
beforeDispatch: (step, actual) => request.dispatch(step, actual).pipe(Effect.orDie),
|
|
72
|
+
onMediaRejected: (reason) => request.onMediaRejected(reason).pipe(Effect.tap((retry) => retry ? Ref.set(cached, Option.none()) : Effect.void), Effect.orDie),
|
|
69
73
|
isComplete,
|
|
70
74
|
pendingInput: () => request.steering.pipe(Effect.orDie),
|
|
71
75
|
onTail: (messages) => Effect.gen(function* () {
|
|
@@ -79,11 +83,12 @@ export const runSteps = (request) => Effect.gen(function* () {
|
|
|
79
83
|
outcome: result.outcome === "ok" ? "completed" : "partial",
|
|
80
84
|
reason: result.reason,
|
|
81
85
|
text: result.finalText,
|
|
82
|
-
steps: yield* Ref.get(steps)
|
|
86
|
+
steps: yield* Ref.get(steps),
|
|
87
|
+
usage: result.usage
|
|
83
88
|
};
|
|
84
89
|
}), StepLoopLive = Layer.succeed(StepLoop, StepLoop.of({ ...STEP_LOOP, run: runSteps })), stepLoopPlugin = definePlugin({
|
|
85
90
|
id: "@xandreed/plugin-agent-loop/steps",
|
|
86
|
-
version: "0.
|
|
91
|
+
version: "0.14.0",
|
|
87
92
|
scope: "runtime",
|
|
88
93
|
config: Schema.Struct({}),
|
|
89
94
|
defaults: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xandreed/plugin-agent-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24",
|
|
6
6
|
"bun": ">=1.3.0"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@xandreed/core": "0.
|
|
17
|
+
"@xandreed/core": "0.14.0",
|
|
18
18
|
"effect": "4.0.7"
|
|
19
19
|
},
|
|
20
20
|
"private": false,
|