feat: renamed subagent handoff flow to use yield instead of submit_result
- Renamed subagent completion flow from `submit_result` to `yield` across SDK tools, prompts, and docs. - Updated executor/task handling to require and parse `yield` calls, replacing legacy submit-result extraction and state flags. - Added `subagent-yield-reminder` and updated system prompts to require `yield` with `result.data` or `result.error`. - Renamed hidden-tool and registration plumbing to `yield`, including discovery helpers and renderer/test surface.
This commit is contained in:
@@ -64,6 +64,6 @@ describe("parseAgentFields", () => {
|
||||
tools: ["Read", "Grep"],
|
||||
});
|
||||
|
||||
expect(fields?.tools).toEqual(["read", "grep", "submit_result"]);
|
||||
expect(fields?.tools).toEqual(["read", "grep", "yield"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,7 +234,6 @@ describe("ModelRegistry runtime provider registration", () => {
|
||||
expect(model?.api).toBe("openai-completions");
|
||||
});
|
||||
|
||||
|
||||
test("headers-only runtime override preserves existing baseUrl across refresh", async () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const modelId = "runtime-headers-only-baseurl-survivor";
|
||||
@@ -292,11 +291,7 @@ describe("ModelRegistry runtime provider registration", () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
expect(registry.find("anthropic", modelId)?.headers?.[sharedHeader]).toBe(configHeaderValue);
|
||||
|
||||
registry.registerProvider(
|
||||
"anthropic",
|
||||
{ headers: { [sharedHeader]: runtimeHeaderValue } },
|
||||
"ext://runtime",
|
||||
);
|
||||
registry.registerProvider("anthropic", { headers: { [sharedHeader]: runtimeHeaderValue } }, "ext://runtime");
|
||||
await expectProviderHeaderAcrossRefresh(registry, "anthropic", sharedHeader, runtimeHeaderValue);
|
||||
|
||||
registry.clearSourceRegistrations("ext://runtime");
|
||||
@@ -433,18 +428,10 @@ describe("ModelRegistry runtime provider registration", () => {
|
||||
const sourceAHeader = "X-Source-A-Header";
|
||||
const sourceBHeader = "X-Source-B-Header";
|
||||
|
||||
registry.registerProvider(
|
||||
providerName,
|
||||
{ headers: { [sourceAHeader]: "from-source-a" } },
|
||||
"ext://a",
|
||||
);
|
||||
registry.registerProvider(providerName, { headers: { [sourceAHeader]: "from-source-a" } }, "ext://a");
|
||||
expectProviderHeader(registry, providerName, sourceAHeader, "from-source-a");
|
||||
|
||||
registry.registerProvider(
|
||||
providerName,
|
||||
{ headers: { [sourceBHeader]: "from-source-b" } },
|
||||
"ext://b",
|
||||
);
|
||||
registry.registerProvider(providerName, { headers: { [sourceBHeader]: "from-source-b" } }, "ext://b");
|
||||
await expectProviderHeaderAcrossRefresh(registry, providerName, sourceAHeader, undefined);
|
||||
expectProviderHeader(registry, providerName, sourceBHeader, "from-source-b");
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { CreateAgentSessionResult } from "../../src/sdk";
|
||||
import * as sdkModule from "../../src/sdk";
|
||||
import type { AgentSession, AgentSessionEvent, PromptOptions } from "../../src/session/agent-session";
|
||||
import type { AuthStorage } from "../../src/session/auth-storage";
|
||||
import { runSubprocess, SUBAGENT_WARNING_MISSING_SUBMIT_RESULT } from "../../src/task/executor";
|
||||
import { runSubprocess, SUBAGENT_WARNING_MISSING_YIELD } from "../../src/task/executor";
|
||||
import type { AgentDefinition } from "../../src/task/types";
|
||||
import { EventBus } from "../../src/utils/event-bus";
|
||||
|
||||
@@ -55,7 +55,7 @@ function createMockSession(
|
||||
sessionManager: {
|
||||
appendSessionInit: () => {},
|
||||
},
|
||||
getActiveToolNames: () => ["read", "submit_result"],
|
||||
getActiveToolNames: () => ["read", "yield"],
|
||||
setActiveToolsByName: async (_toolNames: string[]) => {},
|
||||
subscribe: (listener: (event: AgentSessionEvent) => void) => {
|
||||
listeners.push(listener);
|
||||
@@ -90,7 +90,7 @@ function mockCreateAgentSession(session: AgentSession) {
|
||||
return vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(createSessionResult(session));
|
||||
}
|
||||
|
||||
describe("runSubprocess submit_result reminders", () => {
|
||||
describe("runSubprocess yield reminders", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@@ -114,7 +114,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
enableLsp: false,
|
||||
};
|
||||
|
||||
it("sends reminder prompt when subagent stops without submit_result", async () => {
|
||||
it("sends reminder prompt when subagent stops without yield", async () => {
|
||||
const prompts: string[] = [];
|
||||
const promptOptions: Array<PromptOptions | undefined> = [];
|
||||
const session = createMockSession(({ text, options, promptIndex, emit, state }) => {
|
||||
@@ -129,7 +129,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
emit({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "tool-1",
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Result submitted." }],
|
||||
details: { status: "success", data: { done: true } },
|
||||
@@ -145,12 +145,12 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
expect(promptOptions).toHaveLength(2);
|
||||
expect(promptOptions[0]?.attribution).toBe("agent");
|
||||
expect(promptOptions[1]?.attribution).toBe("agent");
|
||||
expect(prompts[1]).toContain("You stopped without calling submit_result");
|
||||
expect(prompts[1]).toContain("You stopped without calling yield");
|
||||
expect(result.output).toContain('"done": true');
|
||||
expect(result.output.includes("SYSTEM WARNING")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps null submit_result warning when subagent submits success without data", async () => {
|
||||
it("keeps null yield warning when subagent submits success without data", async () => {
|
||||
const session = createMockSession(({ promptIndex, emit, state }) => {
|
||||
if (promptIndex === 1) {
|
||||
const assistant = createAssistantStopMessage("partial output");
|
||||
@@ -161,7 +161,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
emit({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "tool-2",
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Result submitted." }],
|
||||
details: { status: "success" },
|
||||
@@ -173,21 +173,21 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
mockCreateAgentSession(session);
|
||||
|
||||
const result = await runSubprocess({ ...baseOptions, id: "subagent-2" });
|
||||
expect(result.output).toContain("SYSTEM WARNING: Subagent called submit_result with null data.");
|
||||
expect(result.output).toContain("SYSTEM WARNING: Subagent called yield with null data.");
|
||||
});
|
||||
|
||||
it("retries when submit_result tool returns an error before succeeding", async () => {
|
||||
it("retries when yield tool returns an error before succeeding", async () => {
|
||||
const prompts: string[] = [];
|
||||
const session = createMockSession(({ text, promptIndex, emit, state }) => {
|
||||
prompts.push(text);
|
||||
if (promptIndex === 1) {
|
||||
const assistant = createAssistantStopMessage("attempted submit_result");
|
||||
const assistant = createAssistantStopMessage("attempted yield");
|
||||
state.messages.push(assistant);
|
||||
emit({ type: "message_end", message: assistant });
|
||||
emit({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "tool-error",
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Output does not match schema" }],
|
||||
details: { status: "error", error: "Output does not match schema" },
|
||||
@@ -199,7 +199,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
emit({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "tool-success",
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Result submitted." }],
|
||||
details: { status: "success", data: { ok: true } },
|
||||
@@ -221,7 +221,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
emit({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "tool-thinking-fallback",
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Result submitted." }],
|
||||
details: { status: "success", data: { ok: true } },
|
||||
@@ -268,7 +268,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
emit({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: `tool-thinking-override-${index}`,
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Result submitted." }],
|
||||
details: { status: "success", data: { ok: true } },
|
||||
@@ -292,11 +292,11 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
expect(createAgentSessionSpy.mock.calls[0]?.[0]?.thinkingLevel).toBe(cases[0].expectedThinkingLevel);
|
||||
expect(createAgentSessionSpy.mock.calls[1]?.[0]?.thinkingLevel).toBe(cases[1].expectedThinkingLevel);
|
||||
});
|
||||
it("fails after 3 reminders when submit_result is never called for a structured task", async () => {
|
||||
it("fails after 3 reminders when yield is never called for a structured task", async () => {
|
||||
const prompts: string[] = [];
|
||||
const session = createMockSession(({ text, promptIndex, emit, state }) => {
|
||||
prompts.push(text);
|
||||
const assistant = createAssistantStopMessage(promptIndex === 1 ? "did work" : "still no submit_result");
|
||||
const assistant = createAssistantStopMessage(promptIndex === 1 ? "did work" : "still no yield");
|
||||
state.messages.push(assistant);
|
||||
emit({ type: "message_end", message: assistant });
|
||||
});
|
||||
@@ -311,11 +311,11 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
expect(prompts).toHaveLength(4);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.aborted).toBe(false);
|
||||
expect(result.stderr).toBe(SUBAGENT_WARNING_MISSING_SUBMIT_RESULT);
|
||||
expect(result.stderr).toBe(SUBAGENT_WARNING_MISSING_YIELD);
|
||||
expect(result.abortReason).toBeUndefined();
|
||||
});
|
||||
|
||||
it("surfaces abort reason when submit_result reports aborted status", async () => {
|
||||
it("surfaces abort reason when yield reports aborted status", async () => {
|
||||
const session = createMockSession(({ promptIndex, emit, state }) => {
|
||||
if (promptIndex === 1) {
|
||||
const assistant = createAssistantStopMessage("cannot proceed");
|
||||
@@ -325,7 +325,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
emit({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "tool-abort",
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Task aborted: blocked by permissions" }],
|
||||
details: { status: "aborted", error: "blocked by permissions" },
|
||||
@@ -336,7 +336,7 @@ describe("runSubprocess submit_result reminders", () => {
|
||||
|
||||
mockCreateAgentSession(session);
|
||||
|
||||
const result = await runSubprocess({ ...baseOptions, id: "subagent-aborted-submit-result" });
|
||||
const result = await runSubprocess({ ...baseOptions, id: "subagent-aborted-yield" });
|
||||
expect(result.aborted).toBe(true);
|
||||
expect(result.abortReason).toBe("blocked by permissions");
|
||||
});
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
finalizeSubprocessOutput,
|
||||
SUBAGENT_WARNING_MISSING_SUBMIT_RESULT,
|
||||
SUBAGENT_WARNING_NULL_SUBMIT_RESULT,
|
||||
SUBAGENT_WARNING_MISSING_YIELD,
|
||||
SUBAGENT_WARNING_NULL_YIELD,
|
||||
} from "../../src/task/executor";
|
||||
|
||||
describe("subagent warning injection", () => {
|
||||
it("injects null-data warning when submit_result is success without data", () => {
|
||||
it("injects null-data warning when yield is success without data", () => {
|
||||
const result = finalizeSubprocessOutput({
|
||||
rawOutput: "partial output",
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
doneAborted: false,
|
||||
signalAborted: false,
|
||||
submitResultItems: [{ status: "success" }],
|
||||
yieldItems: [{ status: "success" }],
|
||||
outputSchema: undefined,
|
||||
});
|
||||
|
||||
expect(result.rawOutput).toBe(`${SUBAGENT_WARNING_NULL_SUBMIT_RESULT}\n\npartial output`);
|
||||
expect(result.hasSubmitResult).toBe(true);
|
||||
expect(result.rawOutput).toBe(`${SUBAGENT_WARNING_NULL_YIELD}\n\npartial output`);
|
||||
expect(result.hasYield).toBe(true);
|
||||
});
|
||||
|
||||
it("injects missing-submit warning when subagent exits cleanly without submit_result", () => {
|
||||
it("injects missing-submit warning when subagent exits cleanly without yield", () => {
|
||||
const result = finalizeSubprocessOutput({
|
||||
rawOutput: "",
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
doneAborted: false,
|
||||
signalAborted: false,
|
||||
submitResultItems: undefined,
|
||||
yieldItems: undefined,
|
||||
outputSchema: { properties: { ok: { type: "boolean" } } },
|
||||
});
|
||||
|
||||
expect(result.rawOutput).toBe(SUBAGENT_WARNING_MISSING_SUBMIT_RESULT);
|
||||
expect(result.hasSubmitResult).toBe(false);
|
||||
expect(result.rawOutput).toBe(SUBAGENT_WARNING_MISSING_YIELD);
|
||||
expect(result.hasYield).toBe(false);
|
||||
});
|
||||
|
||||
it("does not inject missing-submit warning when fallback completion is recoverable", () => {
|
||||
@@ -43,7 +43,7 @@ describe("subagent warning injection", () => {
|
||||
stderr: "",
|
||||
doneAborted: false,
|
||||
signalAborted: false,
|
||||
submitResultItems: undefined,
|
||||
yieldItems: undefined,
|
||||
outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
|
||||
});
|
||||
|
||||
@@ -58,13 +58,11 @@ describe("subagent warning injection", () => {
|
||||
stderr: "",
|
||||
doneAborted: false,
|
||||
signalAborted: false,
|
||||
submitResultItems: undefined,
|
||||
yieldItems: undefined,
|
||||
outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
|
||||
});
|
||||
|
||||
expect(result.rawOutput).toBe(
|
||||
`${SUBAGENT_WARNING_MISSING_SUBMIT_RESULT}\n\nagent stopped after writing analysis`,
|
||||
);
|
||||
expect(result.rawOutput).toBe(`${SUBAGENT_WARNING_MISSING_YIELD}\n\nagent stopped after writing analysis`);
|
||||
});
|
||||
|
||||
it("does not inject missing-submit warning when execution exits non-zero", () => {
|
||||
@@ -74,7 +72,7 @@ describe("subagent warning injection", () => {
|
||||
stderr: "subagent terminated",
|
||||
doneAborted: true,
|
||||
signalAborted: false,
|
||||
submitResultItems: undefined,
|
||||
yieldItems: undefined,
|
||||
outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
|
||||
});
|
||||
|
||||
@@ -83,32 +81,32 @@ describe("subagent warning injection", () => {
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("normalizes explicit aborted submit_result into aborted payload", () => {
|
||||
it("normalizes explicit aborted yield into aborted payload", () => {
|
||||
const result = finalizeSubprocessOutput({
|
||||
rawOutput: "partial output",
|
||||
exitCode: 1,
|
||||
stderr: "old error",
|
||||
doneAborted: false,
|
||||
signalAborted: false,
|
||||
submitResultItems: [{ status: "aborted", error: "blocked by permissions" }],
|
||||
yieldItems: [{ status: "aborted", error: "blocked by permissions" }],
|
||||
outputSchema: undefined,
|
||||
});
|
||||
|
||||
expect(result.abortedViaSubmitResult).toBe(true);
|
||||
expect(result.abortedViaYield).toBe(true);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stderr).toBe("blocked by permissions");
|
||||
expect(result.rawOutput).toContain('"aborted": true');
|
||||
expect(result.rawOutput).toContain('"blocked by permissions"');
|
||||
});
|
||||
|
||||
it("accepts successful submit_result data without warning", () => {
|
||||
it("accepts successful yield data without warning", () => {
|
||||
const result = finalizeSubprocessOutput({
|
||||
rawOutput: "should be replaced",
|
||||
exitCode: 1,
|
||||
stderr: "should clear",
|
||||
doneAborted: false,
|
||||
signalAborted: false,
|
||||
submitResultItems: [{ status: "success", data: { ok: true } }],
|
||||
yieldItems: [{ status: "success", data: { ok: true } }],
|
||||
outputSchema: undefined,
|
||||
});
|
||||
|
||||
@@ -125,7 +123,7 @@ describe("subagent warning injection", () => {
|
||||
stderr: "",
|
||||
doneAborted: false,
|
||||
signalAborted: false,
|
||||
submitResultItems: undefined,
|
||||
yieldItems: undefined,
|
||||
outputSchema: undefined,
|
||||
});
|
||||
|
||||
|
||||
@@ -177,12 +177,12 @@ describe("createTools", () => {
|
||||
expect(names).toEqual(["report_finding", "exit_plan_mode"]);
|
||||
});
|
||||
|
||||
it("includes submit_result tool when required", async () => {
|
||||
const session = createTestSession({ requireSubmitResultTool: true });
|
||||
it("includes yield tool when required", async () => {
|
||||
const session = createTestSession({ requireYieldTool: true });
|
||||
const tools = await createTools(session);
|
||||
const names = tools.map(t => t.name);
|
||||
|
||||
expect(names).toContain("submit_result");
|
||||
expect(names).toContain("yield");
|
||||
});
|
||||
|
||||
it("excludes ask tool when hasUI is false", async () => {
|
||||
@@ -250,7 +250,7 @@ describe("createTools", () => {
|
||||
"report_finding",
|
||||
"report_tool_issue",
|
||||
"resolve",
|
||||
"submit_result",
|
||||
"yield",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+7
-7
@@ -1,14 +1,14 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import "../../src/tools/submit-result";
|
||||
import "../../src/tools/yield";
|
||||
import { subprocessToolRegistry } from "../../src/task/subprocess-tool-registry";
|
||||
|
||||
describe("submit_result subprocess extraction", () => {
|
||||
const handler = subprocessToolRegistry.getHandler("submit_result");
|
||||
describe("yield subprocess extraction", () => {
|
||||
const handler = subprocessToolRegistry.getHandler("yield");
|
||||
|
||||
it("extracts valid submit_result payloads", () => {
|
||||
it("extracts valid yield payloads", () => {
|
||||
expect(handler?.extractData).toBeDefined();
|
||||
const data = handler?.extractData?.({
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
toolCallId: "call-1",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Result submitted." }],
|
||||
@@ -19,9 +19,9 @@ describe("submit_result subprocess extraction", () => {
|
||||
expect(data).toEqual({ status: "success", data: { ok: true }, error: undefined });
|
||||
});
|
||||
|
||||
it("ignores malformed submit_result details without status", () => {
|
||||
it("ignores malformed yield details without status", () => {
|
||||
const data = handler?.extractData?.({
|
||||
toolName: "submit_result",
|
||||
toolName: "yield",
|
||||
toolCallId: "call-2",
|
||||
result: {
|
||||
content: [{ type: "text", text: "Tool execution was aborted." }],
|
||||
+21
-21
@@ -4,7 +4,7 @@ import { enforceStrictSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
||||
import { validateToolArguments } from "@oh-my-pi/pi-ai/utils/validation";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
||||
import { SubmitResultTool } from "@oh-my-pi/pi-coding-agent/tools/submit-result";
|
||||
import { YieldTool } from "@oh-my-pi/pi-coding-agent/tools/yield";
|
||||
|
||||
function createSession(overrides: Partial<ToolSession> = {}): ToolSession {
|
||||
return {
|
||||
@@ -34,9 +34,9 @@ function getSuccessDataSchema(parameters: Record<string, unknown>): Record<strin
|
||||
throw new Error("Missing success variant with data schema");
|
||||
}
|
||||
|
||||
describe("SubmitResultTool", () => {
|
||||
describe("YieldTool", () => {
|
||||
it("exposes top-level object parameters with required result union", () => {
|
||||
const tool = new SubmitResultTool(createSession());
|
||||
const tool = new YieldTool(createSession());
|
||||
const schema = tool.parameters as {
|
||||
type?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
@@ -48,19 +48,19 @@ describe("SubmitResultTool", () => {
|
||||
});
|
||||
|
||||
it("accepts success payload with data", async () => {
|
||||
const tool = new SubmitResultTool(createSession());
|
||||
const tool = new YieldTool(createSession());
|
||||
const result = await tool.execute("call-1", { result: { data: { ok: true } } } as never);
|
||||
expect(result.details).toEqual({ data: { ok: true }, status: "success", error: undefined });
|
||||
});
|
||||
|
||||
it("accepts aborted payload with error only", async () => {
|
||||
const tool = new SubmitResultTool(createSession());
|
||||
const tool = new YieldTool(createSession());
|
||||
const result = await tool.execute("call-2", { result: { error: "blocked" } } as never);
|
||||
expect(result.details).toEqual({ data: undefined, status: "aborted", error: "blocked" });
|
||||
});
|
||||
|
||||
it("accepts arbitrary data when outputSchema is null", async () => {
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema: null }));
|
||||
const tool = new YieldTool(createSession({ outputSchema: null }));
|
||||
const result = await tool.execute("call-null", { result: { data: { nested: { x: 1 }, ok: true } } } as never);
|
||||
expect(result.details).toEqual({
|
||||
data: { nested: { x: 1 }, ok: true },
|
||||
@@ -70,7 +70,7 @@ describe("SubmitResultTool", () => {
|
||||
});
|
||||
|
||||
it("treats outputSchema true as unconstrained and accepts primitive and array data", async () => {
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema: true }));
|
||||
const tool = new YieldTool(createSession({ outputSchema: true }));
|
||||
const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record<string, unknown>);
|
||||
|
||||
expect(dataSchema.type).toBeUndefined();
|
||||
@@ -85,7 +85,7 @@ describe("SubmitResultTool", () => {
|
||||
});
|
||||
});
|
||||
it("repairs strict schema generation for required-only object output schemas", () => {
|
||||
const tool = new SubmitResultTool(
|
||||
const tool = new YieldTool(
|
||||
createSession({
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
@@ -103,7 +103,7 @@ describe("SubmitResultTool", () => {
|
||||
});
|
||||
|
||||
it("normalizes object/null type arrays into strict-compatible data variants", () => {
|
||||
const tool = new SubmitResultTool(
|
||||
const tool = new YieldTool(
|
||||
createSession({
|
||||
outputSchema: {
|
||||
type: ["object", "null"],
|
||||
@@ -129,7 +129,7 @@ describe("SubmitResultTool", () => {
|
||||
});
|
||||
|
||||
it("converts mixed JTD and JSON Schema output definitions into provider-valid schemas", async () => {
|
||||
const tool = new SubmitResultTool(
|
||||
const tool = new YieldTool(
|
||||
createSession({
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
@@ -189,7 +189,7 @@ describe("SubmitResultTool", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema }));
|
||||
const tool = new YieldTool(createSession({ outputSchema }));
|
||||
const parametersRecord = tool.parameters as unknown as Record<string, unknown>;
|
||||
// $defs should NOT be in parameters — refs are inlined
|
||||
expect(parametersRecord.$defs).toBeUndefined();
|
||||
@@ -232,7 +232,7 @@ describe("SubmitResultTool", () => {
|
||||
]);
|
||||
});
|
||||
it("falls back to unconstrained object data when output schema is invalid", async () => {
|
||||
const tool = new SubmitResultTool(
|
||||
const tool = new YieldTool(
|
||||
createSession({
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
@@ -263,7 +263,7 @@ describe("SubmitResultTool", () => {
|
||||
const circularSchema: Record<string, unknown> = { type: "object" };
|
||||
circularSchema.self = circularSchema;
|
||||
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema: circularSchema }));
|
||||
const tool = new YieldTool(createSession({ outputSchema: circularSchema }));
|
||||
const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record<string, unknown>);
|
||||
|
||||
expect(tool.strict).toBe(false);
|
||||
@@ -299,7 +299,7 @@ describe("SubmitResultTool", () => {
|
||||
return root;
|
||||
};
|
||||
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema: buildDeepSchema(20_000) }));
|
||||
const tool = new YieldTool(createSession({ outputSchema: buildDeepSchema(20_000) }));
|
||||
const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record<string, unknown>);
|
||||
|
||||
expect(tool.strict).toBe(false);
|
||||
@@ -311,7 +311,7 @@ describe("SubmitResultTool", () => {
|
||||
|
||||
it("handles non-object output schemas without blocking successful result submission", async () => {
|
||||
for (const outputSchema of [[], 123, false]) {
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema }));
|
||||
const tool = new YieldTool(createSession({ outputSchema }));
|
||||
const result = await tool.execute("call-non-object-schema", {
|
||||
result: { data: { value: outputSchema } },
|
||||
} as never);
|
||||
@@ -333,7 +333,7 @@ describe("SubmitResultTool", () => {
|
||||
},
|
||||
required: ["token"],
|
||||
};
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema }));
|
||||
const tool = new YieldTool(createSession({ outputSchema }));
|
||||
const dataSchema = getSuccessDataSchema(tool.parameters as unknown as Record<string, unknown>);
|
||||
const tokenSchema = toRecord(toRecord(dataSchema.properties).token);
|
||||
|
||||
@@ -357,7 +357,7 @@ describe("SubmitResultTool", () => {
|
||||
},
|
||||
required: ["token"],
|
||||
};
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema }));
|
||||
const tool = new YieldTool(createSession({ outputSchema }));
|
||||
|
||||
await expect(tool.execute("call-short-1", { result: { data: { token: "ab" } } } as never)).rejects.toThrow(
|
||||
"Output does not match schema",
|
||||
@@ -384,7 +384,7 @@ describe("SubmitResultTool", () => {
|
||||
},
|
||||
required: ["token"],
|
||||
};
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema }));
|
||||
const tool = new YieldTool(createSession({ outputSchema }));
|
||||
|
||||
const firstResult = await tool.execute("call-valid-1", { result: { data: { token: "abcd" } } } as never);
|
||||
expect(firstResult.content).toEqual([{ type: "text", text: "Result submitted." }]);
|
||||
@@ -408,7 +408,7 @@ describe("SubmitResultTool", () => {
|
||||
},
|
||||
required: ["token"],
|
||||
};
|
||||
const tool = new SubmitResultTool(createSession({ outputSchema }));
|
||||
const tool = new YieldTool(createSession({ outputSchema }));
|
||||
|
||||
await expect(tool.execute("call-struct-1", { result: { data: { token: "ab" } } } as never)).rejects.toThrow(
|
||||
"Output does not match schema",
|
||||
@@ -422,13 +422,13 @@ describe("SubmitResultTool", () => {
|
||||
);
|
||||
});
|
||||
it("rejects submissions without a result object", async () => {
|
||||
const tool = new SubmitResultTool(createSession());
|
||||
const tool = new YieldTool(createSession());
|
||||
await expect(tool.execute("call-3", {} as never)).rejects.toThrow(
|
||||
"result must be an object containing either data or error",
|
||||
);
|
||||
});
|
||||
it("sets lenientArgValidation so agent-loop bypasses validation errors", () => {
|
||||
const tool = new SubmitResultTool(createSession());
|
||||
const tool = new YieldTool(createSession());
|
||||
expect(tool.lenientArgValidation).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user