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
14 changes: 14 additions & 0 deletions packages/sdk/src/authored-worker-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,24 @@ export function authoredWorkerRunner(
+ 'if you do not need enforcement, or use the declarative spec\'s `permissions` field, which is real.',
);
}
if (options.cli !== undefined && typeof options.cli !== 'string') {
throw new AuthoredFlowExecutionError(
'agent_cli_unresolved',
`f.agent options.cli must be a string when set (got ${typeof options.cli}).`,
);
}
if (options.model !== undefined && typeof options.model !== 'string') {
throw new AuthoredFlowExecutionError(
'agent_cli_unresolved',
`f.agent options.model must be a string when set (got ${typeof options.model}).`,
);
}
const output = await run({
id, type: 'agent', instruction: options.task,
...(localAgentStream === undefined ? {} : { surfaces: { streams: [{ stream: localAgentStream }] } }),
...(options.workspace === undefined ? {} : { surfaces: { workspace: [{ surface: options.workspace }] } }),
...(options.cli === undefined ? {} : { cli: options.cli }),
...(options.model === undefined ? {} : { model: options.model }),
});
if (typeof output !== 'object' || output === null || Array.isArray(output)) {
throw new AuthoredFlowExecutionError('journal_protocol_violation', `step "${id}" produced a non-object output`);
Expand Down
25 changes: 25 additions & 0 deletions packages/sdk/tests/authored-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,31 @@ describe('authored flow journal executor', () => {
});
});

it('refuses non-string cli or model on f.agent options before contacting the journal', async () => {
const disconnectedJournal = new JournalClient('/journal-must-not-be-contacted');

// Both preflight and the type check throw `agent_cli_unresolved`
// (matching the shared LLM code pattern) — the diagnostic message
// distinguishes them so authors get an accurate error.
for (const bad of [42, true, {}, []]) {
await expect(executeAuthoredFlow(flow('agent-cli-not-string', async (f) => {
await f.agent('worker', { task: 'x', cli: bad as unknown as string });
f.done('success');
}), disconnectedJournal)).rejects.toMatchObject({
code: 'agent_cli_unresolved',
message: expect.stringMatching(/f\.agent options\.cli must be a string when set/),
});

await expect(executeAuthoredFlow(flow('agent-model-not-string', async (f) => {
await f.agent('worker', { task: 'x', model: bad as unknown as string });
f.done('success');
}), disconnectedJournal)).rejects.toMatchObject({
code: 'agent_cli_unresolved',
message: expect.stringMatching(/f\.agent options\.model must be a string when set/),
});
}
});

it('rejects invalid raw headers before the executor can contact the journal', async () => {
const disconnectedJournal = new JournalClient('/journal-must-not-be-contacted');

Expand Down
12 changes: 7 additions & 5 deletions packages/sdk/tests/live-kernel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,13 @@ process.stdin.on('end', () => {
});
`);
chmodSync(cli, 0o755);
// f.agent has no way to declare a CLI itself (AgentOptions is
// {task, workspace} only) — it resolves one exactly the way a bare
// `type: agent` YAML step with no explicit `cli` does: the nearest
// flows.json's project default, found searching upward from the flow's
// own path (checkAuthoredFlow, cli/check.ts).
// This test intentionally omits cli/model on f.agent (AgentOptions
// accepts { task, workspace, cli?, model? } as of #310) — it exercises
// the resolution fallback: with no step cli, an authored agent resolves
// exactly like a bare `type: agent` YAML step, walking up to the
// nearest flows.json's project default (checkAuthoredFlow, cli/check.ts).
// The explicit-cli/model path is covered by unit tests in
// authored-flow.test.ts.
writeFileSync(join(directory, 'flows.json'), JSON.stringify({ cli }));
await startDaemon(dataDir);

Expand Down
2 changes: 2 additions & 0 deletions packages/surface/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface AgentResult {
export interface AgentOptions {
task: string;
workspace?: string;
cli?: string;
model?: string;
}

export interface LlmOptions {
Expand Down
Loading