Skip to content

Commit 2d34f56

Browse files
committed
minor improvements
1 parent 56450de commit 2d34f56

6 files changed

Lines changed: 190 additions & 50 deletions

File tree

‎README.md‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,41 @@ Invoke OpenCode tools:
123123
```
124124

125125
### Agent Step
126-
Prompt an LLM:
126+
Invoke a named OpenCode agent or prompt an LLM directly:
127+
128+
**Named Agent (recommended):**
129+
```json
130+
{
131+
"id": "security-review",
132+
"type": "agent",
133+
"agent": "security-reviewer",
134+
"prompt": "Review this code for security issues:\n\n{{steps.read_file.result}}",
135+
"maxTokens": 1000
136+
}
137+
```
138+
139+
This invokes a pre-defined OpenCode agent by name. The agent's system prompt, model, and other settings are configured in OpenCode's agent definitions.
140+
141+
**Inline LLM (fallback):**
127142
```json
128143
{
129144
"id": "generate-changelog",
130145
"type": "agent",
131146
"prompt": "Generate a changelog for version {{inputs.version}}",
132-
"model": "gpt-4",
147+
"system": "You are a technical writer.",
133148
"maxTokens": 1000
134149
}
135150
```
136151

152+
This makes a direct LLM call with an optional system prompt. Note that `model` selection may not be supported by the plugin system - the configured default model will be used.
153+
154+
| Option | Type | Description |
155+
|--------|------|-------------|
156+
| `agent` | `string` | Name of a pre-defined OpenCode agent to invoke |
157+
| `prompt` | `string` | The prompt to send (required, supports interpolation) |
158+
| `system` | `string` | System prompt for inline LLM calls (ignored if `agent` is specified) |
159+
| `maxTokens` | `number` | Maximum tokens for response |
160+
137161
### Suspend Step
138162
Pause for human input:
139163
```json
@@ -381,33 +405,29 @@ This example chains multiple specialized agents to review code from different pe
381405
{
382406
"id": "security_review",
383407
"type": "agent",
384-
"system": "You are a security expert. Identify vulnerabilities, injection risks, and auth issues. Be concise.",
408+
"agent": "security-reviewer",
385409
"prompt": "Review this code for security issues:\n\n{{steps.read_file.result}}",
386-
"model": "anthropic:claude-sonnet-4-20250514",
387410
"after": ["read_file"]
388411
},
389412
{
390413
"id": "perf_review",
391414
"type": "agent",
392-
"system": "You are a performance engineer. Identify bottlenecks, memory leaks, and optimization opportunities. Be concise.",
415+
"agent": "performance-reviewer",
393416
"prompt": "Review this code for performance issues:\n\n{{steps.read_file.result}}",
394-
"model": "anthropic:claude-sonnet-4-20250514",
395417
"after": ["read_file"]
396418
},
397419
{
398420
"id": "quality_review",
399421
"type": "agent",
400-
"system": "You are a senior developer. Review for readability, maintainability, and best practices. Be concise.",
422+
"agent": "quality-reviewer",
401423
"prompt": "Review this code for quality issues:\n\n{{steps.read_file.result}}",
402-
"model": "anthropic:claude-sonnet-4-20250514",
403424
"after": ["read_file"]
404425
},
405426
{
406427
"id": "synthesize",
407428
"type": "agent",
408-
"system": "You are a tech lead. Synthesize code reviews into a prioritized action list grouped by severity.",
429+
"agent": "tech-lead",
409430
"prompt": "Combine these reviews into a single report:\n\n## Security\n{{steps.security_review.response}}\n\n## Performance\n{{steps.perf_review.response}}\n\n## Quality\n{{steps.quality_review.response}}",
410-
"model": "anthropic:claude-sonnet-4-20250514",
411431
"after": ["security_review", "perf_review", "quality_review"]
412432
},
413433
{
@@ -419,15 +439,16 @@ This example chains multiple specialized agents to review code from different pe
419439
{
420440
"id": "generate_fixes",
421441
"type": "agent",
422-
"system": "You are a code fixer. Output ONLY the corrected code, no explanations.",
442+
"agent": "code-fixer",
423443
"prompt": "Fix the critical and high severity issues:\n\nOriginal:\n{{steps.read_file.result}}\n\nIssues:\n{{steps.synthesize.response}}",
424-
"model": "anthropic:claude-sonnet-4-20250514",
425444
"after": ["approve_fixes"]
426445
}
427446
]
428447
}
429448
```
430449

450+
> **Note**: This example assumes you have agents named `security-reviewer`, `performance-reviewer`, `quality-reviewer`, `tech-lead`, and `code-fixer` configured in OpenCode. Alternatively, you can use inline LLM calls with `system` prompts instead of named agents.
451+
431452
Run it with:
432453
```
433454
/workflow run code-review file=src/api/auth.ts

‎examples/code-review.json‎

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
"version": "1.0.0",
66
"tags": ["review", "agents", "quality"],
77
"inputs": {
8-
"file": {
9-
"type": "string",
10-
"description": "File path to review",
11-
"required": true
12-
}
8+
"file": "string"
139
},
1410
"steps": [
1511
{
@@ -25,40 +21,32 @@
2521
"id": "security_review",
2622
"type": "agent",
2723
"description": "Security vulnerability analysis",
28-
"system": "You are a security expert. Identify vulnerabilities, injection risks, authentication issues, and data exposure. Be concise and specific.",
24+
"agent": "security-reviewer",
2925
"prompt": "Review this code for security issues:\n\n{{steps.read_file.result}}",
30-
"model": "anthropic:claude-sonnet-4-20250514",
31-
"maxTokens": 1000,
3226
"after": ["read_file"]
3327
},
3428
{
3529
"id": "perf_review",
3630
"type": "agent",
3731
"description": "Performance analysis",
38-
"system": "You are a performance engineer. Identify bottlenecks, memory leaks, N+1 queries, and optimization opportunities. Be concise and specific.",
32+
"agent": "performance-reviewer",
3933
"prompt": "Review this code for performance issues:\n\n{{steps.read_file.result}}",
40-
"model": "anthropic:claude-sonnet-4-20250514",
41-
"maxTokens": 1000,
4234
"after": ["read_file"]
4335
},
4436
{
4537
"id": "quality_review",
4638
"type": "agent",
4739
"description": "Code quality analysis",
48-
"system": "You are a senior developer. Review for readability, maintainability, error handling, and best practices. Be concise and specific.",
40+
"agent": "quality-reviewer",
4941
"prompt": "Review this code for quality issues:\n\n{{steps.read_file.result}}",
50-
"model": "anthropic:claude-sonnet-4-20250514",
51-
"maxTokens": 1000,
5242
"after": ["read_file"]
5343
},
5444
{
5545
"id": "synthesize",
5646
"type": "agent",
5747
"description": "Synthesize reviews into prioritized action items",
58-
"system": "You are a tech lead. Synthesize multiple code reviews into a single prioritized action list. Group issues by severity: critical, high, medium, low. Be actionable.",
48+
"agent": "tech-lead",
5949
"prompt": "Combine these code reviews into a prioritized report:\n\n## Security Review\n{{steps.security_review.response}}\n\n## Performance Review\n{{steps.perf_review.response}}\n\n## Quality Review\n{{steps.quality_review.response}}",
60-
"model": "anthropic:claude-sonnet-4-20250514",
61-
"maxTokens": 2000,
6250
"after": ["security_review", "perf_review", "quality_review"]
6351
},
6452
{
@@ -72,10 +60,8 @@
7260
"id": "generate_fixes",
7361
"type": "agent",
7462
"description": "Generate fixed code",
75-
"system": "You are a code fixer. Apply the requested fixes to the code. Output ONLY the complete corrected code with no explanations or markdown.",
63+
"agent": "code-fixer",
7664
"prompt": "Fix the critical and high severity issues in this code:\n\nOriginal code:\n{{steps.read_file.result}}\n\nIssues to fix:\n{{steps.synthesize.response}}",
77-
"model": "anthropic:claude-sonnet-4-20250514",
78-
"maxTokens": 4000,
7965
"after": ["approve_fixes"]
8066
}
8167
]

‎src/adapters/steps.test.ts‎

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,97 @@ describe("Step Adapters", () => {
730730
});
731731
});
732732

733-
describe("agent execution", () => {
733+
describe("named agent execution", () => {
734+
it("should invoke named agent when agent property is specified", async () => {
735+
const mockInvoke = vi.fn().mockResolvedValue({ content: "Agent response" });
736+
mockClient.agents = {
737+
"code-reviewer": { invoke: mockInvoke },
738+
};
739+
740+
const step = createAgentStep(
741+
{ id: "review", type: "agent", agent: "code-reviewer", prompt: "Review this code" },
742+
mockClient
743+
);
744+
745+
const result = await step.execute({
746+
inputData: { inputs: {}, steps: {} },
747+
} as unknown as Parameters<typeof step.execute>[0]);
748+
749+
expect(result).toEqual({ response: "Agent response" });
750+
expect(mockInvoke).toHaveBeenCalledWith("Review this code", { maxTokens: undefined });
751+
expect(mockClient.llm.chat).not.toHaveBeenCalled();
752+
});
753+
754+
it("should pass maxTokens to named agent", async () => {
755+
const mockInvoke = vi.fn().mockResolvedValue({ content: "Response" });
756+
mockClient.agents = {
757+
summarizer: { invoke: mockInvoke },
758+
};
759+
760+
const step = createAgentStep(
761+
{ id: "summary", type: "agent", agent: "summarizer", prompt: "Summarize", maxTokens: 500 },
762+
mockClient
763+
);
764+
765+
await step.execute({
766+
inputData: { inputs: {}, steps: {} },
767+
} as unknown as Parameters<typeof step.execute>[0]);
768+
769+
expect(mockInvoke).toHaveBeenCalledWith("Summarize", { maxTokens: 500 });
770+
});
771+
772+
it("should interpolate prompt for named agent", async () => {
773+
const mockInvoke = vi.fn().mockResolvedValue({ content: "Response" });
774+
mockClient.agents = {
775+
reviewer: { invoke: mockInvoke },
776+
};
777+
778+
const step = createAgentStep(
779+
{ id: "review", type: "agent", agent: "reviewer", prompt: "Review: {{inputs.code}}" },
780+
mockClient
781+
);
782+
783+
await step.execute({
784+
inputData: { inputs: { code: "const x = 1" }, steps: {} },
785+
} as unknown as Parameters<typeof step.execute>[0]);
786+
787+
expect(mockInvoke).toHaveBeenCalledWith("Review: const x = 1", { maxTokens: undefined });
788+
});
789+
790+
it("should throw error when agent is not found", async () => {
791+
mockClient.agents = {
792+
"other-agent": { invoke: vi.fn() },
793+
};
794+
795+
const step = createAgentStep(
796+
{ id: "review", type: "agent", agent: "unknown-agent", prompt: "Test" },
797+
mockClient
798+
);
799+
800+
await expect(
801+
step.execute({
802+
inputData: { inputs: {}, steps: {} },
803+
} as unknown as Parameters<typeof step.execute>[0])
804+
).rejects.toThrow("Agent 'unknown-agent' not found. Available agents: other-agent");
805+
});
806+
807+
it("should throw error when no agents are available", async () => {
808+
// mockClient.agents is undefined by default
809+
810+
const step = createAgentStep(
811+
{ id: "review", type: "agent", agent: "any-agent", prompt: "Test" },
812+
mockClient
813+
);
814+
815+
await expect(
816+
step.execute({
817+
inputData: { inputs: {}, steps: {} },
818+
} as unknown as Parameters<typeof step.execute>[0])
819+
).rejects.toThrow("No agents available on the opencode client");
820+
});
821+
});
822+
823+
describe("inline LLM execution", () => {
734824
it("should call LLM with prompt and return response", async () => {
735825
const step = createAgentStep(
736826
{ id: "agent", type: "agent", prompt: "Summarize this" },
@@ -743,7 +833,6 @@ describe("Step Adapters", () => {
743833

744834
expect(result).toEqual({ response: "LLM response" });
745835
expect(mockClient.llm.chat).toHaveBeenCalledWith({
746-
model: undefined,
747836
messages: [{ role: "user", content: "Summarize this" }],
748837
maxTokens: undefined,
749838
});
@@ -793,13 +882,12 @@ describe("Step Adapters", () => {
793882
);
794883
});
795884

796-
it("should pass model and maxTokens config", async () => {
885+
it("should pass maxTokens config", async () => {
797886
const step = createAgentStep(
798887
{
799888
id: "agent",
800889
type: "agent",
801890
prompt: "Hello",
802-
model: "gpt-4",
803891
maxTokens: 500,
804892
},
805893
mockClient
@@ -810,7 +898,6 @@ describe("Step Adapters", () => {
810898
} as unknown as Parameters<typeof step.execute>[0]);
811899

812900
expect(mockClient.llm.chat).toHaveBeenCalledWith({
813-
model: "gpt-4",
814901
messages: [{ role: "user", content: "Hello" }],
815902
maxTokens: 500,
816903
});
@@ -851,6 +938,23 @@ describe("Step Adapters", () => {
851938
expect(result).toEqual({ response: "", skipped: true });
852939
expect(mockClient.llm.chat).not.toHaveBeenCalled();
853940
});
941+
942+
it("should skip named agent when condition is false", async () => {
943+
const mockInvoke = vi.fn();
944+
mockClient.agents = { reviewer: { invoke: mockInvoke } };
945+
946+
const step = createAgentStep(
947+
{ id: "agent", type: "agent", agent: "reviewer", prompt: "Review", condition: "{{inputs.run}}" },
948+
mockClient
949+
);
950+
951+
const result = await step.execute({
952+
inputData: { inputs: { run: "false" }, steps: {} },
953+
} as unknown as Parameters<typeof step.execute>[0]);
954+
955+
expect(result).toEqual({ response: "", skipped: true });
956+
expect(mockInvoke).not.toHaveBeenCalled();
957+
});
854958
});
855959
});
856960

‎src/adapters/steps.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,16 @@ export function createToolStep(def: ToolStepDefinition, client: OpencodeClient)
341341
// =============================================================================
342342

343343
/**
344-
* Creates a Mastra step that prompts an LLM
344+
* Creates a Mastra step that invokes an agent or prompts an LLM.
345+
*
346+
* Supports two modes:
347+
* 1. Named agent reference: Uses `def.agent` to invoke a pre-defined opencode agent
348+
* 2. Inline LLM call: Uses `def.system` for direct LLM chat (legacy/fallback)
345349
*/
346350
export function createAgentStep(def: AgentStepDefinition, client: OpencodeClient) {
347351
return createStep({
348352
id: def.id,
349-
description: def.description || "LLM Agent prompt",
353+
description: def.description || (def.agent ? `Agent: ${def.agent}` : "LLM prompt"),
350354
inputSchema: StepInputSchema,
351355
outputSchema: z.object({
352356
response: z.string(),
@@ -381,9 +385,26 @@ export function createAgentStep(def: AgentStepDefinition, client: OpencodeClient
381385

382386
// Interpolate prompt
383387
const prompt = interpolate(def.prompt, ctx);
384-
385-
// Log prompt to TUI
386-
client.app.log(`Agent prompt: ${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}`, "info");
388+
389+
// Mode 1: Named agent reference
390+
if (def.agent) {
391+
if (!client.agents) {
392+
throw new Error("No agents available on the opencode client. Ensure agents are configured.");
393+
}
394+
395+
const agent = client.agents[def.agent];
396+
if (!agent) {
397+
const availableAgents = Object.keys(client.agents).join(", ") || "(none)";
398+
throw new Error(`Agent '${def.agent}' not found. Available agents: ${availableAgents}`);
399+
}
400+
401+
client.app.log(`Invoking agent: ${def.agent}`, "info");
402+
const response = await agent.invoke(prompt, { maxTokens: def.maxTokens });
403+
return { response: response.content };
404+
}
405+
406+
// Mode 2: Inline LLM call (legacy/fallback)
407+
client.app.log(`LLM prompt: ${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}`, "info");
387408

388409
const messages: Array<{ role: string; content: string }> = [];
389410

@@ -397,7 +418,6 @@ export function createAgentStep(def: AgentStepDefinition, client: OpencodeClient
397418
messages.push({ role: "user", content: prompt });
398419

399420
const response = await client.llm.chat({
400-
model: def.model,
401421
messages,
402422
maxTokens: def.maxTokens,
403423
});

‎src/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ function createClientAdapter(client: PluginInput["client"]): OpencodeClient {
6262

6363
const response = await llmClient.llm.chat({
6464
messages: opts.messages,
65-
model: opts.model,
6665
maxTokens: opts.maxTokens,
6766
});
6867

0 commit comments

Comments
 (0)