Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/agent/director.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ class ChatDirectorImpl extends DefaultDirector {
logger.warn`workflow-coordinator-directive-not-string`;
return null;
}
if (directive.length === 0) return null;
if (directive.length > MAX_WORKFLOW_DIRECTIVE_CHARS) {
logger.warn`workflow-coordinator-directive-truncated chars=${String(directive.length)} max=${String(MAX_WORKFLOW_DIRECTIVE_CHARS)}`;
return `${directive.slice(0, MAX_WORKFLOW_DIRECTIVE_CHARS)}\n…[truncated]`;
Expand All @@ -618,7 +619,13 @@ class ChatDirectorImpl extends DefaultDirector {

private coordinatorCurrentStepId(): string | null {
try {
return this.workflowCoordinator?.currentStepId() ?? null;
const stepId = this.workflowCoordinator?.currentStepId() ?? null;
if (stepId === null) return null;
if (typeof stepId !== "string") {
logger.warn`workflow-coordinator-step-id-not-string`;
return null;
}
return stepId;
} catch (err) {
logger.warn`workflow-coordinator-step-id-threw error=${err instanceof Error ? err.message : String(err)}`;
return null;
Expand Down
66 changes: 66 additions & 0 deletions src/director.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,72 @@ describe("CL-7919 coordinator shape", () => {
MAX_WORKFLOW_DIRECTIVE_CHARS + "…[truncated]".length + 1,
);
});

// A non-string step id never reaches prompt text: the stall nudge falls
// back to the generic clause instead of interpolating the foreign value.
test("a non-string step id falls back to the generic submit_output clause", async () => {
const director = createChatDirector("base-prompt", [], {
onTasksChange: () => undefined,
});
director.setWorkflowCoordinator({
directive: () => "do the thing",
isActive: () => true,
currentStepIsGate: () => false,
currentStepId: () => 42,
handleToolDone: () => false,
} as unknown as WorkflowCoordinator);
const actions = actionsArray(
await director.decide(
{
type: "inference.done",
turn: {
role: "assistant",
model: "test",
timestamp: 0,
content: [{ type: "text", text: "all set" }],
},
usage: {
input: 10,
output: 1,
cacheRead: 0,
cacheWrite: 0,
thinking: 0,
},
source: { model: "test-model" },
} as unknown as ReactorInboundEvent,
mockState,
capabilitiesWithInferArgs,
),
);
const text = inferEphemeralText(actions.find((a) => a.type === "infer"));
expect(text).toContain("call submit_output with this step's id now");
expect(text).not.toContain("42");
});

// An empty directive is absent guidance: no ephemeral turn is appended
// and the turn resolves as plain inference.
test("an empty-string directive resolves as plain inference", async () => {
const director = createChatDirector("base-prompt", [], {
onTasksChange: () => undefined,
});
director.setWorkflowCoordinator({
directive: () => "",
isActive: () => true,
currentStepIsGate: () => false,
currentStepId: () => "a",
handleToolDone: () => false,
} as unknown as WorkflowCoordinator);
const actions = actionsArray(
await director.decide(
makeMessageReceivedEvent("hello"),
mockState,
capabilitiesWithInferArgs,
),
);
const infer = actions.find((a) => a.type === "infer");
expect(infer).toBeDefined();
expect(inferEphemeralText(infer)).toBeUndefined();
});
});

describe("submit_output workflow handler", () => {
Expand Down
Loading