Skip to content

Commit b5ecef5

Browse files
committed
feat: enhance adapter functionality with LangGraph and DeepAgents support, update documentation, and improve example scripts
1 parent 08d3d13 commit b5ecef5

12 files changed

Lines changed: 167 additions & 18 deletions

File tree

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ Add your adapter to the switch statement that dispatches on `-a <adapter>`.
133133

134134
If your framework has a config file format, add an exporter in `commands/export.ts` or `src/adapters/`.
135135

136+
If your example directory commits an `expected_output.py` (or similar) snapshot, regenerate it whenever you change the adapter so it does not drift:
137+
138+
```bash
139+
gapman export --dir examples/<name> --format <name> --output examples/<name>/expected_output.py
140+
```
141+
136142
### 4. Document what's lossy
137143

138144
Every adapter loses something — that's expected. Please document it clearly:

examples/deepagents/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A research agent with a fact-checker sub-agent, demonstrating the DeepAgents ada
44

55
- `agent.yaml` — manifest (model, skills, tools, sub-agent declaration)
66
- `SOUL.md` — agent identity, embedded in the generated `system_prompt`
7-
- `skills/web-research/`, `skills/summarize/` — passed via `skills=["./skills"]`; DeepAgents loads each `SKILL.md` natively
7+
- `skills/web-research/`, `skills/summarize/` — passed via a `skills=` path resolved next to the generated module (`Path(__file__).parent / "skills"`), so discovery works from any working directory; DeepAgents loads each `SKILL.md` natively
88
- `tools/web-search.yaml` — emitted as a `@tool` function bound into `tools=[...]`
99
- `agents/fact-checker/` — emitted as a `SubAgent` dict in `subagents=[...]`
1010
- `expected_output.py` — the Python module the adapter produces
@@ -27,4 +27,10 @@ pip install deepagents langchain-anthropic
2727
python examples/deepagents/expected_output.py
2828
```
2929

30+
Or let gitagent generate and run it for you (pass an initial message with `-p`, read from `GITAGENT_PROMPT`):
31+
32+
```bash
33+
gapman run --dir examples/deepagents --adapter deepagents -p "Who won the 2022 World Cup?"
34+
```
35+
3036
The generated file leaves tool implementations as `NotImplementedError` stubs — replace them with your own logic before invoking.

examples/deepagents/expected_output.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from deepagents import create_deep_agent
99
from langchain_core.tools import tool
10+
from pathlib import Path
1011

1112
# Agent metadata
1213
AGENT_NAME = "research-assistant"
@@ -43,9 +44,8 @@ def web_search(query: str) -> str:
4344
TOOLS = [web_search]
4445

4546
# Skills (skills/<name>/SKILL.md — DeepAgents loads these natively)
46-
# Pointing skills= at the directory lets DeepAgents discover every SKILL.md
47-
# without us having to inline the skill content into SYSTEM_PROMPT.
48-
SKILLS = ["./skills"]
47+
# Resolved relative to this file so discovery works from any working directory.
48+
SKILLS = [str(Path(__file__).resolve().parent / "skills")]
4949

5050
# For reference, the skills available in this agent:
5151
# - summarize: Condense the gathered sources into a faithful, cited summary
@@ -78,6 +78,8 @@ def web_search(query: str) -> str:
7878
)
7979

8080
if __name__ == "__main__":
81-
result = agent.invoke({"messages": [{"role": "user", "content": "Hello"}]})
81+
import os
82+
user_input = os.environ.get("GITAGENT_PROMPT", "Hello")
83+
result = agent.invoke({"messages": [{"role": "user", "content": user_input}]})
8284
for message in result["messages"]:
8385
print(message)

examples/langgraph/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,10 @@ pip install "langgraph>=0.2" "langchain>=0.3" "langchain-core>=0.3" langchain-an
2323
python examples/langgraph/expected_output.py
2424
```
2525

26+
Or let gitagent generate and run it for you (pass an initial message with `-p`, read from `GITAGENT_PROMPT`):
27+
28+
```bash
29+
gapman run --dir examples/langgraph --adapter langgraph -p "Summarize the latest on RISC-V adoption"
30+
```
31+
2632
The generated file leaves tool implementations as `NotImplementedError` stubs — replace them with your own logic before invoking.

examples/langgraph/expected_output.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ def skill_web_research(state: AgentState) -> dict:
101101
app = graph.compile()
102102

103103
if __name__ == "__main__":
104+
import os
105+
user_input = os.environ.get("GITAGENT_PROMPT", "Hello")
104106
result = app.invoke(
105-
{"messages": [HumanMessage(content="Hello")]},
107+
{"messages": [HumanMessage(content=user_input)]},
106108
config={"recursion_limit": RECURSION_LIMIT},
107109
)
108110
for message in result["messages"]:

src/adapters/deepagents.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,16 @@ describe('exportToDeepAgents', () => {
155155
assert.match(code, /TOOLS = \[web_search\]/);
156156
});
157157

158-
test('skills/ becomes skills=SKILLS pointing at "./skills"', () => {
158+
test('skills/ becomes skills=SKILLS resolved relative to the module', () => {
159159
const dir = makeAgentDir({
160160
skills: [
161161
{ name: 'research', description: 'Research a topic', instructions: 'Cite sources.' },
162162
],
163163
});
164164
const { code } = exportToDeepAgents(dir);
165-
assert.match(code, /SKILLS = \["\.\/skills"\]/);
165+
assert.match(code, /from pathlib import Path/);
166+
assert.match(code, /SKILLS = \[str\(Path\(__file__\)\.resolve\(\)\.parent \/ "skills"\)\]/);
166167
assert.match(code, /skills=SKILLS,/);
167-
// The skill metadata is enumerated as a reference comment.
168168
assert.match(code, /#\s+- research:/);
169169
});
170170

src/adapters/deepagents.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function renderPython(ctx: RenderContext): string {
178178
const { manifest, systemPrompt, skills, tools, preToolUseScripts, subAgents } = ctx;
179179
const lines: string[] = [];
180180
const model = manifest.model?.preferred ?? 'anthropic:claude-sonnet-4-5';
181+
const needsPath = skills.length > 0 || subAgents.some(s => s.hasSkills);
181182

182183
// Header
183184
lines.push('"""');
@@ -191,6 +192,7 @@ function renderPython(ctx: RenderContext): string {
191192
lines.push('');
192193
lines.push('from deepagents import create_deep_agent');
193194
lines.push('from langchain_core.tools import tool');
195+
if (needsPath) lines.push('from pathlib import Path');
194196
lines.push('');
195197

196198
// Agent metadata
@@ -247,9 +249,8 @@ function renderPython(ctx: RenderContext): string {
247249
// Skills — DeepAgents loads SKILL.md natively from directory paths
248250
lines.push('# Skills (skills/<name>/SKILL.md — DeepAgents loads these natively)');
249251
if (skills.length > 0) {
250-
lines.push('# Pointing skills= at the directory lets DeepAgents discover every SKILL.md');
251-
lines.push('# without us having to inline the skill content into SYSTEM_PROMPT.');
252-
lines.push('SKILLS = ["./skills"]');
252+
lines.push('# Resolved relative to this file so discovery works from any working directory.');
253+
lines.push('SKILLS = [str(Path(__file__).resolve().parent / "skills")]');
253254
lines.push('');
254255
lines.push('# For reference, the skills available in this agent:');
255256
for (const skill of skills) {
@@ -271,7 +272,7 @@ function renderPython(ctx: RenderContext): string {
271272
lines.push(` "system_prompt": ${pyTripleStr(sub.systemPrompt)},`);
272273
lines.push(' "tools": TOOLS,');
273274
if (sub.hasSkills) {
274-
lines.push(` "skills": [${pyStr(`./agents/${sub.name}/skills`)}],`);
275+
lines.push(` "skills": [str(Path(__file__).resolve().parent / "agents" / ${pyStr(sub.name)} / "skills")],`);
275276
}
276277
lines.push('}');
277278
lines.push('');
@@ -299,7 +300,9 @@ function renderPython(ctx: RenderContext): string {
299300

300301
// CLI entry point
301302
lines.push('if __name__ == "__main__":');
302-
lines.push(' result = agent.invoke({"messages": [{"role": "user", "content": "Hello"}]})');
303+
lines.push(' import os');
304+
lines.push(' user_input = os.environ.get("GITAGENT_PROMPT", "Hello")');
305+
lines.push(' result = agent.invoke({"messages": [{"role": "user", "content": user_input}]})');
303306
lines.push(' for message in result["messages"]:');
304307
lines.push(' print(message)');
305308
lines.push('');

src/adapters/langgraph.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ export function exportToLangGraph(dir: string): LangGraphExport {
5656
const preToolUseScripts = collectPreToolUseHooks(agentDir);
5757
const subAgents = collectSubAgents(agentDir);
5858

59+
const unreferencedSkills = findUnreferencedSkills(skills, flows);
60+
for (const name of unreferencedSkills) {
61+
console.warn(
62+
`gitagent: skill "${name}" is declared but no skillflow step references it; it will not be reachable in the compiled graph.`,
63+
);
64+
}
65+
5966
const code = renderPython({
6067
manifest,
6168
systemPrompt,
@@ -64,11 +71,23 @@ export function exportToLangGraph(dir: string): LangGraphExport {
6471
flows,
6572
preToolUseScripts,
6673
subAgents,
74+
unreferencedSkills,
6775
});
6876

6977
return { code };
7078
}
7179

80+
function findUnreferencedSkills(skills: ParsedSkill[], flows: Skillflow[]): string[] {
81+
if (flows.length === 0) return [];
82+
const referenced = new Set<string>();
83+
for (const flow of flows) {
84+
for (const step of flow.steps) {
85+
if (step.skill) referenced.add(step.skill);
86+
}
87+
}
88+
return skills.map(s => s.frontmatter.name).filter(name => !referenced.has(name));
89+
}
90+
7291
export function exportToLangGraphString(dir: string): string {
7392
return exportToLangGraph(dir).code;
7493
}
@@ -212,10 +231,11 @@ interface RenderContext {
212231
flows: Skillflow[];
213232
preToolUseScripts: string[];
214233
subAgents: string[];
234+
unreferencedSkills: string[];
215235
}
216236

217237
function renderPython(ctx: RenderContext): string {
218-
const { manifest, systemPrompt, skills, tools, flows, preToolUseScripts, subAgents } = ctx;
238+
const { manifest, systemPrompt, skills, tools, flows, preToolUseScripts, subAgents, unreferencedSkills } = ctx;
219239

220240
const lines: string[] = [];
221241
const recursionLimit = manifest.runtime?.max_turns ?? 25;
@@ -345,6 +365,14 @@ function renderPython(ctx: RenderContext): string {
345365
}
346366
}
347367

368+
if (unreferencedSkills.length > 0) {
369+
lines.push('# Declared but unreferenced by any skillflow — defined above, never wired below:');
370+
for (const name of unreferencedSkills) {
371+
lines.push(`# - ${escapeForComment(name)}`);
372+
}
373+
lines.push('');
374+
}
375+
348376
// Sub-agent stubs (nested compiled StateGraphs)
349377
if (subAgents.length > 0) {
350378
lines.push('# Sub-agents (agents/<name>/) — nested compiled StateGraphs');
@@ -474,8 +502,10 @@ function renderPython(ctx: RenderContext): string {
474502
lines.push('app = graph.compile()');
475503
lines.push('');
476504
lines.push('if __name__ == "__main__":');
505+
lines.push(' import os');
506+
lines.push(' user_input = os.environ.get("GITAGENT_PROMPT", "Hello")');
477507
lines.push(' result = app.invoke(');
478-
lines.push(' {"messages": [HumanMessage(content="Hello")]},');
508+
lines.push(' {"messages": [HumanMessage(content=user_input)]},');
479509
lines.push(' config={"recursion_limit": RECURSION_LIMIT},');
480510
lines.push(' )');
481511
lines.push(' for message in result["messages"]:');

src/commands/run.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { runWithGit } from '../runners/git.js';
1515
import { runWithOpenCode } from '../runners/opencode.js';
1616
import { runWithGemini } from '../runners/gemini.js';
1717
import { runWithGitclaw } from '../runners/gitclaw.js';
18+
import { runWithLangGraph } from '../runners/langgraph.js';
19+
import { runWithDeepAgents } from '../runners/deepagents.js';
1820

1921
interface RunOptions {
2022
repo?: string;
@@ -30,7 +32,7 @@ interface RunOptions {
3032
export const runCommand = new Command('run')
3133
.description('Run an agent from a git repository or local directory')
3234
.option('-r, --repo <url>', 'Git repository URL')
33-
.option('-a, --adapter <name>', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, gitclaw, git, prompt', 'claude')
35+
.option('-a, --adapter <name>', 'Adapter: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, gitclaw, langgraph, deepagents, git, prompt', 'claude')
3436
.option('-b, --branch <branch>', 'Git branch/tag to clone', 'main')
3537
.option('--refresh', 'Force re-clone (pull latest)', false)
3638
.option('--no-cache', 'Clone to temp dir, delete on exit')
@@ -129,6 +131,12 @@ export const runCommand = new Command('run')
129131
case 'gitclaw':
130132
runWithGitclaw(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
131133
break;
134+
case 'langgraph':
135+
runWithLangGraph(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
136+
break;
137+
case 'deepagents':
138+
runWithDeepAgents(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
139+
break;
132140
case 'git':
133141
if (!options.repo) {
134142
error('The git adapter requires --repo (-r)');
@@ -148,7 +156,7 @@ export const runCommand = new Command('run')
148156
break;
149157
default:
150158
error(`Unknown adapter: ${options.adapter}`);
151-
info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, gitclaw, git, prompt');
159+
info('Supported adapters: claude, openai, crewai, openclaw, nanobot, lyzr, github, opencode, gemini, gitclaw, langgraph, deepagents, git, prompt');
152160
process.exit(1);
153161
}
154162
} catch (e) {

src/runners/deepagents.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { exportToDeepAgentsString } from '../adapters/deepagents.js';
2+
import { AgentManifest } from '../utils/loader.js';
3+
import { runPythonModule, type PythonRunOptions } from './python.js';
4+
5+
const INSTALL_HINT = 'Install dependencies: pip install deepagents langchain-anthropic';
6+
7+
export function runWithDeepAgents(
8+
agentDir: string,
9+
_manifest: AgentManifest,
10+
options: PythonRunOptions = {},
11+
): void {
12+
const code = exportToDeepAgentsString(agentDir);
13+
runPythonModule(agentDir, code, 'deepagents', INSTALL_HINT, options);
14+
}

0 commit comments

Comments
 (0)