agency-lang 0.7.6 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as smoltalk from "smoltalk";
|
|
2
2
|
import { UserContentInput } from "smoltalk";
|
|
3
|
+
import { type FuncParam } from "./agencyFunction.js";
|
|
3
4
|
import type { RetryPolicy, RetryConfig, LLMRetryReason } from "./llmRetry.js";
|
|
4
5
|
import type { NormalizedLLMError } from "./llmClient.js";
|
|
5
6
|
import type { SourceLocationOpts } from "./state/checkpointStore.js";
|
|
@@ -26,6 +27,17 @@ export declare function redactMessagesForLog(messages: MessageThread): smoltalk.
|
|
|
26
27
|
/** A prompt with attachment payloads redacted, preserving its string-or-array
|
|
27
28
|
* shape, for statelog / hook data. */
|
|
28
29
|
export declare function redactPromptForLog(p: string | UserContentInput): string | UserContentInput;
|
|
30
|
+
/** LLMs routinely emit an explicit `null` for an optional tool argument
|
|
31
|
+
* they chose not to set (e.g. `bash(command, cwd: null, timeout: null)`).
|
|
32
|
+
* Agency default-parameter values only fill `undefined`, so a `null` would
|
|
33
|
+
* sail past the default into the function body — `bash(cwd: null)` reaches
|
|
34
|
+
* `applyAgentCwd` → `path.resolve(base, null)` and throws. Drop any argument
|
|
35
|
+
* whose value is `null` when its parameter declares a default, so the call
|
|
36
|
+
* behaves exactly as if the LLM had omitted the key (the default applies).
|
|
37
|
+
* Params without a default keep their value untouched: a required arg passed
|
|
38
|
+
* `null` still surfaces its normal type error for the model to correct, and
|
|
39
|
+
* an intentionally-nullable param is left alone. */
|
|
40
|
+
declare function dropNullDefaultedArgs(args: Record<string, any> | null | undefined, params: readonly FuncParam[]): Record<string, any>;
|
|
29
41
|
/** Coerce an arbitrary tool result to the string the LLM would see.
|
|
30
42
|
* Strings pass through; everything else is JSON-stringified, with a
|
|
31
43
|
* `String()` fallback for values JSON can't represent (e.g. circular). */
|
|
@@ -92,6 +104,7 @@ export declare const _internal: {
|
|
|
92
104
|
assertUniqueToolNames: typeof assertUniqueToolNames;
|
|
93
105
|
armCallTimeout: typeof armCallTimeout;
|
|
94
106
|
runWithRetry: typeof runWithRetry;
|
|
107
|
+
dropNullDefaultedArgs: typeof dropNullDefaultedArgs;
|
|
95
108
|
};
|
|
96
109
|
export declare function runPrompt(args: {
|
|
97
110
|
prompt: string | UserContentInput;
|
|
@@ -77,6 +77,25 @@ async function dispatchLLMRequest({ ctx, promptConfig, prompt, stream, stateStac
|
|
|
77
77
|
toolCalls: response.value.toolCalls || [],
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
/** LLMs routinely emit an explicit `null` for an optional tool argument
|
|
81
|
+
* they chose not to set (e.g. `bash(command, cwd: null, timeout: null)`).
|
|
82
|
+
* Agency default-parameter values only fill `undefined`, so a `null` would
|
|
83
|
+
* sail past the default into the function body — `bash(cwd: null)` reaches
|
|
84
|
+
* `applyAgentCwd` → `path.resolve(base, null)` and throws. Drop any argument
|
|
85
|
+
* whose value is `null` when its parameter declares a default, so the call
|
|
86
|
+
* behaves exactly as if the LLM had omitted the key (the default applies).
|
|
87
|
+
* Params without a default keep their value untouched: a required arg passed
|
|
88
|
+
* `null` still surfaces its normal type error for the model to correct, and
|
|
89
|
+
* an intentionally-nullable param is left alone. */
|
|
90
|
+
function dropNullDefaultedArgs(args, params) {
|
|
91
|
+
const out = { ...args };
|
|
92
|
+
for (const param of params) {
|
|
93
|
+
if (param.hasDefault && out[param.name] === null) {
|
|
94
|
+
delete out[param.name];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
80
99
|
/** Default cap on characters of a single tool result fed back to the
|
|
81
100
|
* LLM. A recursive `ls`/`grep` can return megabytes; without a cap one
|
|
82
101
|
* tool call can blow the context window. The FULL result is still
|
|
@@ -273,6 +292,7 @@ export const _internal = {
|
|
|
273
292
|
assertUniqueToolNames,
|
|
274
293
|
armCallTimeout,
|
|
275
294
|
runWithRetry,
|
|
295
|
+
dropNullDefaultedArgs,
|
|
276
296
|
};
|
|
277
297
|
async function _runPrompt({ ctx, messages, tools, prompt, responseFormat, clientConfig, stateStack, retryPolicy, }) {
|
|
278
298
|
if (ctx.isCancelled(stateStack)) {
|
|
@@ -908,7 +928,7 @@ export async function runPrompt(args) {
|
|
|
908
928
|
// uniformly by completedSteps inside b.step (start/invoke/end
|
|
909
929
|
// each get marked done on success and skipped on resume).
|
|
910
930
|
const branchStack = stack.getOrCreateBranch(branchKey).stack;
|
|
911
|
-
const namedArgs =
|
|
931
|
+
const namedArgs = dropNullDefaultedArgs(toolCall.arguments, handler.params);
|
|
912
932
|
await b.step(`round.${round}.tool.${callSlug}.start`, async () => {
|
|
913
933
|
// Pass `branchStack` so scoped callbacks registered inside
|
|
914
934
|
// the branch's frame chain are discovered by
|
|
@@ -246,3 +246,41 @@ describe("runWithRetry", () => {
|
|
|
246
246
|
await expect(_internal.runWithRetry(dispatch, policy, parent.signal, noHooks, normalize)).rejects.toSatisfy((e) => readCause(e)?.kind === "userInterrupt");
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
|
+
describe("dropNullDefaultedArgs", () => {
|
|
250
|
+
const P = (name, hasDefault) => ({
|
|
251
|
+
name,
|
|
252
|
+
hasDefault,
|
|
253
|
+
defaultValue: undefined,
|
|
254
|
+
variadic: false,
|
|
255
|
+
});
|
|
256
|
+
it("drops a null argument when the param has a default (LLM omitted it)", () => {
|
|
257
|
+
// Mirrors the real bug: bash(command, cwd: null) — cwd has a default of "".
|
|
258
|
+
const params = [P("command", false), P("cwd", true), P("timeout", true)];
|
|
259
|
+
const out = _internal.dropNullDefaultedArgs({ command: "ls", cwd: null, timeout: null }, params);
|
|
260
|
+
expect(out).toEqual({ command: "ls" });
|
|
261
|
+
expect("cwd" in out).toBe(false);
|
|
262
|
+
expect("timeout" in out).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
it("keeps a null argument when the param has NO default", () => {
|
|
265
|
+
// A required (or intentionally-nullable) param keeps its value so the
|
|
266
|
+
// normal type error still surfaces for the model to correct.
|
|
267
|
+
const params = [P("target", false)];
|
|
268
|
+
expect(_internal.dropNullDefaultedArgs({ target: null }, params))
|
|
269
|
+
.toEqual({ target: null });
|
|
270
|
+
});
|
|
271
|
+
it("leaves non-null values untouched, including falsy ones", () => {
|
|
272
|
+
const params = [P("cwd", true), P("count", true), P("flag", true)];
|
|
273
|
+
const out = _internal.dropNullDefaultedArgs({ cwd: "/app", count: 0, flag: false }, params);
|
|
274
|
+
expect(out).toEqual({ cwd: "/app", count: 0, flag: false });
|
|
275
|
+
});
|
|
276
|
+
it("handles null/undefined argument objects", () => {
|
|
277
|
+
expect(_internal.dropNullDefaultedArgs(null, [P("cwd", true)])).toEqual({});
|
|
278
|
+
expect(_internal.dropNullDefaultedArgs(undefined, [P("cwd", true)])).toEqual({});
|
|
279
|
+
});
|
|
280
|
+
it("does not drop keys absent from the param list", () => {
|
|
281
|
+
// An arg with no matching param (e.g. an LLM hallucinated key) is left
|
|
282
|
+
// as-is; only declared defaulted params are considered.
|
|
283
|
+
const out = _internal.dropNullDefaultedArgs({ extra: null }, [P("cwd", true)]);
|
|
284
|
+
expect(out).toEqual({ extra: null });
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.7.
|
|
1
|
+
export declare const VERSION = "0.7.7";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.7.
|
|
1
|
+
export const VERSION = "0.7.7";
|