@xandreed/plugin-agent-loop 0.12.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/index.d.ts +1 -3
- package/dist/index.js +1 -3
- 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 +7 -7
- package/dist/plugin.adapter.d.ts +0 -12
- package/dist/plugin.adapter.js +0 -54
- package/dist/runAgent.d.ts +0 -141
- package/dist/runAgent.js +0 -63
- package/dist/runAgent.test.d.ts +0 -1
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
export * from "./loop.js";
|
|
2
|
-
export
|
|
3
|
-
export { agentLoopPlugin, default } from "./plugin.adapter.js";
|
|
4
|
-
export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin } from "./step-loop.adapter.js";
|
|
2
|
+
export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin, stepLoopPlugin as default } from "./step-loop.adapter.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
export * from "./loop.js";
|
|
2
|
-
export
|
|
3
|
-
export { agentLoopPlugin, default } from "./plugin.adapter.js";
|
|
4
|
-
export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin } from "./step-loop.adapter.js";
|
|
2
|
+
export { runSteps, STEP_LOOP, StepLoopLive, stepLoopPlugin, stepLoopPlugin as default } from "./step-loop.adapter.js";
|
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,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xandreed/plugin-agent-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=24",
|
|
6
|
+
"bun": ">=1.3.0"
|
|
7
|
+
},
|
|
4
8
|
"type": "module",
|
|
5
9
|
"main": "./dist/index.js",
|
|
6
10
|
"exports": {
|
|
@@ -10,8 +14,8 @@
|
|
|
10
14
|
}
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
|
-
"@xandreed/core": "0.
|
|
14
|
-
"effect": "4.0.
|
|
17
|
+
"@xandreed/core": "0.14.0",
|
|
18
|
+
"effect": "4.0.7"
|
|
15
19
|
},
|
|
16
20
|
"private": false,
|
|
17
21
|
"types": "./dist/index.d.ts",
|
|
@@ -22,9 +26,5 @@
|
|
|
22
26
|
"README.md",
|
|
23
27
|
"LICENSE"
|
|
24
28
|
],
|
|
25
|
-
"engines": {
|
|
26
|
-
"node": ">=24",
|
|
27
|
-
"bun": ">=1.3.0"
|
|
28
|
-
},
|
|
29
29
|
"license": "MIT"
|
|
30
30
|
}
|
package/dist/plugin.adapter.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { LanguageModel } from "effect/ai";
|
|
2
|
-
import { AgentLoop, AgentTools, ContextManager, Memory, SessionStore } from "@xandreed/core";
|
|
3
|
-
export declare const agentLoopPlugin: import("@xandreed/core").TypedPlugin<{
|
|
4
|
-
readonly maxSteps: number;
|
|
5
|
-
readonly toolConcurrency: number;
|
|
6
|
-
readonly streaming: boolean;
|
|
7
|
-
}, {
|
|
8
|
-
readonly maxSteps: number;
|
|
9
|
-
readonly toolConcurrency: number;
|
|
10
|
-
readonly streaming: boolean;
|
|
11
|
-
}, AgentLoop, never, LanguageModel.LanguageModel | SessionStore | AgentTools | ContextManager | Memory>;
|
|
12
|
-
export default agentLoopPlugin;
|
package/dist/plugin.adapter.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { LanguageModel } from "effect/ai";
|
|
2
|
-
import { Effect, Layer, Option, Ref, Schema } from "effect";
|
|
3
|
-
import { AgentLoop, AgentMessage, AgentTools, ContextManager, definePlugin, handoffToMessage, HarnessError, Memory, SessionStore } from "@xandreed/core";
|
|
4
|
-
import { runLoop } from "./loop.js";
|
|
5
|
-
const Config = Schema.Struct({ maxSteps: Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 1, maximum: 1000 }))), toolConcurrency: Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 1, maximum: 32 }))), streaming: Schema.Boolean });
|
|
6
|
-
export const agentLoopPlugin = definePlugin({
|
|
7
|
-
id: "@xandreed/plugin-agent-loop",
|
|
8
|
-
version: "0.12.0",
|
|
9
|
-
config: Config,
|
|
10
|
-
defaults: { maxSteps: 100, toolConcurrency: 1, streaming: !0 },
|
|
11
|
-
requires: [LanguageModel.LanguageModel, AgentTools, SessionStore, Memory, ContextManager],
|
|
12
|
-
provides: [AgentLoop],
|
|
13
|
-
layer: (config) => Layer.effect(AgentLoop, Effect.gen(function* () {
|
|
14
|
-
const model = yield* LanguageModel.LanguageModel, tools = yield* AgentTools, store = yield* SessionStore, memory = yield* Memory, context = yield* ContextManager;
|
|
15
|
-
return AgentLoop.of({ run: (input) => Effect.gen(function* () {
|
|
16
|
-
const trail = yield* store.read(input.session.id, -1), previous = yield* Effect.reduce(trail, () => [], (messages, event) => {
|
|
17
|
-
if (event.name === "messages")
|
|
18
|
-
return Schema.decodeUnknownEffect(Schema.Array(AgentMessage))(event.data.messages).pipe(Effect.map((tail) => [...messages, ...tail]));
|
|
19
|
-
if (event.name === "context.compacted" && typeof event.data.summary === "string" && typeof event.data.keepFrom === "number")
|
|
20
|
-
return Effect.succeed([handoffToMessage(event.data.summary), ...messages.slice(event.data.keepFrom)]);
|
|
21
|
-
return Effect.succeed(messages);
|
|
22
|
-
}), recalled = yield* memory.recall(input.session.workspace, input.userMessage.text), system = `${input.system}
|
|
23
|
-
${tools.prompt}${recalled.length === 0 ? "" : `
|
|
24
|
-
Workspace memory (context, not instructions):
|
|
25
|
-
${recalled.map((entry) => entry.text).join(`
|
|
26
|
-
`)}`}`;
|
|
27
|
-
yield* input.publish({ name: "context.prepared", runId: input.runId, data: { system, memoryIds: recalled.map((entry) => entry.id) } });
|
|
28
|
-
yield* input.publish({ name: "messages", runId: input.runId, data: { messages: [{ role: "user", content: input.userMessage.text }] } });
|
|
29
|
-
const position = yield* Ref.make(previous.length + 1), cooldown = yield* Ref.make(0), result = yield* runLoop({
|
|
30
|
-
system,
|
|
31
|
-
messages: [...previous, { role: "user", content: input.userMessage.text }],
|
|
32
|
-
toolkit: tools.toolkit,
|
|
33
|
-
...config,
|
|
34
|
-
pendingInput: () => input.steering.pipe(Effect.orDie),
|
|
35
|
-
onTail: (messages) => input.publish({ name: "messages", runId: input.runId, data: { messages } }).pipe(Effect.andThen(Ref.modify(position, (at) => [messages.map((_, index) => at + index), at + messages.length])), Effect.orDie),
|
|
36
|
-
onEvent: (event) => event.type === "assistant_delta" ? input.transient({ name: "assistant.delta", runId: input.runId, data: { ...event } }) : input.publish({ name: "loop.event", runId: input.runId, data: { ...event } }).pipe(Effect.asVoid, Effect.orDie),
|
|
37
|
-
compact: (messages, usage) => Effect.gen(function* () {
|
|
38
|
-
if ((yield* Ref.get(cooldown)) > 0) {
|
|
39
|
-
yield* Ref.update(cooldown, (value) => value - 1);
|
|
40
|
-
return Option.none();
|
|
41
|
-
}
|
|
42
|
-
const compacted = yield* context.compact(messages, usage.inputTokens);
|
|
43
|
-
if (Option.isSome(compacted)) {
|
|
44
|
-
yield* input.publish({ name: "context.compacted", runId: input.runId, data: { ...compacted.value } });
|
|
45
|
-
yield* Ref.set(cooldown, 3);
|
|
46
|
-
}
|
|
47
|
-
return compacted;
|
|
48
|
-
}).pipe(Effect.catch((error) => input.publish({ name: "context.failed", runId: input.runId, data: { message: error.message } }).pipe(Effect.as(Option.none()), Effect.orDie)))
|
|
49
|
-
}).pipe(Effect.provideService(LanguageModel.LanguageModel, model), Effect.provide(tools.handlers));
|
|
50
|
-
return { text: result.finalText, outcome: result.outcome === "ok" ? "completed" : "partial" };
|
|
51
|
-
}).pipe(Effect.mapError((error) => new HarnessError({ code: "loop.failed", message: String(error) }))) });
|
|
52
|
-
}))
|
|
53
|
-
});
|
|
54
|
-
export default agentLoopPlugin;
|
package/dist/runAgent.d.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import type { Prompt, Tool, Toolkit } from "effect/ai";
|
|
2
|
-
import { Effect, Option } from "effect";
|
|
3
|
-
import type { LoopEvent } from "@xandreed/core";
|
|
4
|
-
import type { AgentMessage, ConversationId } from "@xandreed/core";
|
|
5
|
-
import { ConversationStore } from "@xandreed/core";
|
|
6
|
-
import type { ModelCallPolicy } from "@xandreed/core";
|
|
7
|
-
import type { UserMessage } from "@xandreed/core";
|
|
8
|
-
/**
|
|
9
|
-
* WITHIN-RUN compaction policy: when a finished turn's `inputTokens` exceeds
|
|
10
|
-
* the threshold, the buffer's older turns fold into a summary and the run
|
|
11
|
-
* continues on summary + the last `keepTurns` assistant turns verbatim (the
|
|
12
|
-
* kept tail is ALSO covered by the summary — the overlap that survives a
|
|
13
|
-
* boundary). `summarize` sees the whole buffer; any failure or an empty
|
|
14
|
-
* summary skips the fold (best-effort — the run continues unfolded).
|
|
15
|
-
*/
|
|
16
|
-
export interface CompactionPolicy {
|
|
17
|
-
readonly thresholdTokens: number;
|
|
18
|
-
readonly keepTurns: number;
|
|
19
|
-
readonly summarize: (transcript: ReadonlyArray<AgentMessage>, previous: Option.Option<string>) => Effect.Effect<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* An agent is a system prompt + a toolkit (+ its loop bounds). The driver
|
|
23
|
-
* decides which config runs; the engine stays agent-agnostic.
|
|
24
|
-
*/
|
|
25
|
-
export interface AgentConfig<Tools extends Record<string, Tool.Any>> {
|
|
26
|
-
readonly system: string | Prompt.Prompt;
|
|
27
|
-
readonly toolkit: Toolkit.Toolkit<Tools>;
|
|
28
|
-
readonly maxSteps?: number;
|
|
29
|
-
/** Handler concurrency within one model turn. Agents with mutating tools
|
|
30
|
-
* should set this to 1 unless their toolkit provides stronger scheduling. */
|
|
31
|
-
readonly toolConcurrency?: number;
|
|
32
|
-
readonly pollableTools?: ReadonlyArray<string>;
|
|
33
|
-
readonly compaction?: CompactionPolicy;
|
|
34
|
-
/** Stream turns (`assistant_delta` events while tokens flow); falls back
|
|
35
|
-
* to settled turns on a pre-first-part failure. Default false. */
|
|
36
|
-
readonly streaming?: boolean;
|
|
37
|
-
/** Per-agent output/reasoning policy. Dedicated agents pin this instead of
|
|
38
|
-
* inheriting provider-family defaults. */
|
|
39
|
-
readonly modelPolicy?: ModelCallPolicy;
|
|
40
|
-
/** Server-side prompt-cache lane override. Defaults to the conversation id
|
|
41
|
-
* (one lane per conversation); an agent whose system prompt is a shared
|
|
42
|
-
* cacheable prefix may key by ITS hash instead so prefill cache hits fire
|
|
43
|
-
* across conversations (probe-verified safe under concurrency on the
|
|
44
|
-
* codex route, 2026-07-17). */
|
|
45
|
-
readonly promptCacheKey?: string;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* One user turn over a persisted conversation: append the user message, load the
|
|
49
|
-
* active window (prepending the latest fold's summary when one exists), run
|
|
50
|
-
* the loop with incremental tail persistence, return the result. Store
|
|
51
|
-
* failures are defects (`orDie`) — persistence breaking mid-run is
|
|
52
|
-
* infrastructure death, not a condition the model can correct.
|
|
53
|
-
*
|
|
54
|
-
* With a `compaction` policy, runAgent also owns the store reconciliation
|
|
55
|
-
* for mid-run folds: a POSITION MIRROR tracks each buffer entry's durable
|
|
56
|
-
* position (None for synthetic handoffs), and a fold writes `checkpointAt`
|
|
57
|
-
* on the last covered position — so `listActive` for the NEXT run returns
|
|
58
|
-
* exactly the kept rows and folded events are never resent.
|
|
59
|
-
*/
|
|
60
|
-
export declare const runAgent: <Tools extends Record<string, Tool.Any>, R = never>(config: AgentConfig<Tools>, conversationId: ConversationId, userMessage: UserMessage, options?: {
|
|
61
|
-
readonly onEvent?: (event: LoopEvent) => Effect.Effect<void, never, R>;
|
|
62
|
-
/** The mid-turn steering seam — see `RunLoopOptions.pendingInput`. */
|
|
63
|
-
readonly pendingInput?: () => Effect.Effect<Option.Option<string>, never, R>;
|
|
64
|
-
/** The user message is redacted from traces by default. Trusted local
|
|
65
|
-
* drivers may opt in explicitly for debugging. */
|
|
66
|
-
readonly traceContent?: boolean;
|
|
67
|
-
}) => Effect.Effect<{
|
|
68
|
-
finalText: string;
|
|
69
|
-
messages: readonly ({
|
|
70
|
-
readonly role: "user";
|
|
71
|
-
readonly content: string;
|
|
72
|
-
readonly providerOptions?: unknown;
|
|
73
|
-
} | {
|
|
74
|
-
readonly role: "assistant";
|
|
75
|
-
readonly content: readonly ({
|
|
76
|
-
readonly type: "text";
|
|
77
|
-
readonly text: string;
|
|
78
|
-
readonly providerOptions?: unknown;
|
|
79
|
-
} | {
|
|
80
|
-
readonly type: "reasoning";
|
|
81
|
-
readonly text: string;
|
|
82
|
-
readonly providerOptions?: unknown;
|
|
83
|
-
} | {
|
|
84
|
-
readonly type: "tool-call";
|
|
85
|
-
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
86
|
-
readonly toolName: string;
|
|
87
|
-
readonly input: unknown;
|
|
88
|
-
readonly providerOptions?: unknown;
|
|
89
|
-
readonly providerExecuted?: boolean | undefined;
|
|
90
|
-
})[];
|
|
91
|
-
readonly providerOptions?: unknown;
|
|
92
|
-
} | {
|
|
93
|
-
readonly role: "tool";
|
|
94
|
-
readonly content: readonly {
|
|
95
|
-
readonly type: "tool-result";
|
|
96
|
-
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
97
|
-
readonly toolName: string;
|
|
98
|
-
readonly output: unknown;
|
|
99
|
-
readonly providerOptions?: unknown;
|
|
100
|
-
readonly isError?: boolean | undefined;
|
|
101
|
-
}[];
|
|
102
|
-
readonly providerOptions?: unknown;
|
|
103
|
-
})[];
|
|
104
|
-
newTail: readonly ({
|
|
105
|
-
readonly role: "user";
|
|
106
|
-
readonly content: string;
|
|
107
|
-
readonly providerOptions?: unknown;
|
|
108
|
-
} | {
|
|
109
|
-
readonly role: "assistant";
|
|
110
|
-
readonly content: readonly ({
|
|
111
|
-
readonly type: "text";
|
|
112
|
-
readonly text: string;
|
|
113
|
-
readonly providerOptions?: unknown;
|
|
114
|
-
} | {
|
|
115
|
-
readonly type: "reasoning";
|
|
116
|
-
readonly text: string;
|
|
117
|
-
readonly providerOptions?: unknown;
|
|
118
|
-
} | {
|
|
119
|
-
readonly type: "tool-call";
|
|
120
|
-
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
121
|
-
readonly toolName: string;
|
|
122
|
-
readonly input: unknown;
|
|
123
|
-
readonly providerOptions?: unknown;
|
|
124
|
-
readonly providerExecuted?: boolean | undefined;
|
|
125
|
-
})[];
|
|
126
|
-
readonly providerOptions?: unknown;
|
|
127
|
-
} | {
|
|
128
|
-
readonly role: "tool";
|
|
129
|
-
readonly content: readonly {
|
|
130
|
-
readonly type: "tool-result";
|
|
131
|
-
readonly toolCallId: string & import("effect/Brand").Brand<"ToolCallId">;
|
|
132
|
-
readonly toolName: string;
|
|
133
|
-
readonly output: unknown;
|
|
134
|
-
readonly providerOptions?: unknown;
|
|
135
|
-
readonly isError?: boolean | undefined;
|
|
136
|
-
}[];
|
|
137
|
-
readonly providerOptions?: unknown;
|
|
138
|
-
})[];
|
|
139
|
-
outcome: "ok" | "partial";
|
|
140
|
-
reason: "completed" | "step-cap" | "degenerate-loop";
|
|
141
|
-
}, import("effect/ai/AiError").AiError, ConversationStore | import("effect/ai/LanguageModel").LanguageModel | Exclude<Exclude<Exclude<Exclude<R, import("effect/Tracer").ParentSpan>, never>, import("effect/Tracer").ParentSpan>, never> | Exclude<Exclude<Exclude<Exclude<Tool.HandlersFor<Tools>, import("effect/Tracer").ParentSpan>, never>, import("effect/Tracer").ParentSpan>, never>>;
|
package/dist/runAgent.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Effect, Option, Ref } from "effect";
|
|
2
|
-
import { ConversationStore } from "@xandreed/core";
|
|
3
|
-
import { CurrentPromptCacheKey } from "@xandreed/core";
|
|
4
|
-
import { CurrentModelCallPolicy } from "@xandreed/core";
|
|
5
|
-
import { handoffToMessage, safeKeepFrom } from "@xandreed/core";
|
|
6
|
-
import { runLoop } from "./loop.js";
|
|
7
|
-
const COMPACT_COOLDOWN_TURNS = 3;
|
|
8
|
-
export const runAgent = (config, conversationId, userMessage, options) => Effect.gen(function* () {
|
|
9
|
-
const store = yield* ConversationStore;
|
|
10
|
-
yield* store.append(conversationId, { role: "user", content: userMessage.text }).pipe(Effect.orDie);
|
|
11
|
-
const fold = yield* store.latestCheckpoint(conversationId).pipe(Effect.orDie), active = yield* store.listActive(conversationId).pipe(Effect.orDie), messages = Option.match(fold, {
|
|
12
|
-
onNone: () => active.map((row) => row.message),
|
|
13
|
-
onSome: (checkpoint) => [
|
|
14
|
-
handoffToMessage(checkpoint.summary),
|
|
15
|
-
...active.map((row) => row.message)
|
|
16
|
-
]
|
|
17
|
-
}), activePositions = active.map((row) => Option.some(row.position)), mirrorRef = yield* Ref.make(Option.isSome(fold) ? [Option.none(), ...activePositions] : activePositions), cooldownRef = yield* Ref.make(0), onTail = (tail) => store.appendAll(conversationId, tail).pipe(Effect.tap((positions) => Ref.update(mirrorRef, (all) => [...all, ...positions.map(Option.some)])), Effect.orDie), compactWith = (policy) => (buffer, lastTurnUsage) => Effect.gen(function* () {
|
|
18
|
-
const cooldown = yield* Ref.get(cooldownRef);
|
|
19
|
-
if (cooldown > 0) {
|
|
20
|
-
yield* Ref.set(cooldownRef, cooldown - 1);
|
|
21
|
-
return Option.none();
|
|
22
|
-
}
|
|
23
|
-
if (lastTurnUsage.inputTokens <= policy.thresholdTokens)
|
|
24
|
-
return Option.none();
|
|
25
|
-
const cut = safeKeepFrom(buffer, policy.keepTurns);
|
|
26
|
-
if (Option.isNone(cut))
|
|
27
|
-
return Option.none();
|
|
28
|
-
const mirror = yield* Ref.get(mirrorRef);
|
|
29
|
-
if (mirror.length !== buffer.length)
|
|
30
|
-
return Option.none();
|
|
31
|
-
const covered = mirror.slice(0, cut.value).flatMap(Option.toArray), coveredPosition = covered[covered.length - 1], firstKept = mirror.slice(cut.value).flatMap(Option.toArray)[0];
|
|
32
|
-
if (coveredPosition === void 0 || firstKept !== void 0 && coveredPosition >= firstKept)
|
|
33
|
-
return Option.none();
|
|
34
|
-
const previous = yield* store.latestCheckpoint(conversationId).pipe(Effect.map(Option.map((checkpoint) => checkpoint.summary))), summary = (yield* policy.summarize(buffer, previous)).trim();
|
|
35
|
-
if (summary.length === 0)
|
|
36
|
-
return Option.none();
|
|
37
|
-
yield* store.checkpointAt(conversationId, summary, coveredPosition);
|
|
38
|
-
yield* Ref.set(mirrorRef, [Option.none(), ...mirror.slice(cut.value)]);
|
|
39
|
-
yield* Ref.set(cooldownRef, COMPACT_COOLDOWN_TURNS);
|
|
40
|
-
return Option.some({ summary, keepFrom: cut.value });
|
|
41
|
-
}).pipe(Effect.withSpan("engine.compact"), Effect.catch(() => Effect.succeed(Option.none()))), result = yield* runLoop({
|
|
42
|
-
system: config.system,
|
|
43
|
-
messages,
|
|
44
|
-
toolkit: config.toolkit,
|
|
45
|
-
...config.maxSteps !== void 0 ? { maxSteps: config.maxSteps } : {},
|
|
46
|
-
...config.toolConcurrency !== void 0 ? { toolConcurrency: config.toolConcurrency } : {},
|
|
47
|
-
...config.pollableTools !== void 0 ? { pollableTools: config.pollableTools } : {},
|
|
48
|
-
...options?.onEvent !== void 0 ? { onEvent: options.onEvent } : {},
|
|
49
|
-
...options?.pendingInput !== void 0 ? { pendingInput: options.pendingInput } : {},
|
|
50
|
-
...config.compaction !== void 0 ? { compact: compactWith(config.compaction) } : {},
|
|
51
|
-
...config.streaming !== void 0 ? { streaming: config.streaming } : {},
|
|
52
|
-
onTail
|
|
53
|
-
}).pipe(Effect.provideService(CurrentPromptCacheKey, Option.some(config.promptCacheKey ?? String(conversationId))));
|
|
54
|
-
yield* store.recordOutcome(conversationId, result.outcome, result.reason).pipe(Effect.catch((error) => Effect.logWarning(`the run's outcome was not recorded: ${error.message}`)));
|
|
55
|
-
return result;
|
|
56
|
-
}).pipe(Effect.withSpan("agent.run", {
|
|
57
|
-
attributes: {
|
|
58
|
-
"agent.kind": "run",
|
|
59
|
-
"agent.conversation_id": String(conversationId),
|
|
60
|
-
"agent.userMessage": options?.traceContent === !0 ? userMessage.text.slice(0, 500) : "[redacted]",
|
|
61
|
-
"agent.userMessage_chars": userMessage.text.length
|
|
62
|
-
}
|
|
63
|
-
}), Effect.provideService(CurrentModelCallPolicy, Option.fromNullishOr(config.modelPolicy)));
|
package/dist/runAgent.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|