@agile-vibe-coding/avc 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -12
- package/cli/agents/code-implementer.md +33 -46
- package/cli/init.js +4 -3
- package/cli/llm-claude.js +72 -0
- package/cli/llm-gemini.js +76 -0
- package/cli/llm-local.js +52 -0
- package/cli/llm-openai.js +52 -0
- package/cli/llm-provider.js +12 -0
- package/cli/llm-xiaomi.js +51 -0
- package/cli/seed-processor.js +31 -0
- package/cli/worktree-runner.js +268 -26
- package/cli/worktree-tools.js +322 -0
- package/kanban/client/dist/assets/index-BSm2Zo5j.js +380 -0
- package/kanban/client/dist/assets/index-BevZLADh.css +1 -0
- package/kanban/client/dist/index.html +2 -2
- package/kanban/client/src/App.jsx +37 -5
- package/kanban/client/src/components/ceremony/RunModal.jsx +329 -0
- package/kanban/client/src/components/ceremony/SeedModal.jsx +2 -2
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +95 -21
- package/kanban/client/src/components/kanban/RunButton.jsx +34 -153
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +4 -0
- package/kanban/client/src/lib/api.js +10 -0
- package/kanban/client/src/store/filterStore.js +10 -3
- package/kanban/client/src/store/runStore.js +103 -0
- package/kanban/server/routes/work-items.js +101 -2
- package/kanban/server/workers/run-task-worker.js +60 -11
- package/package.json +1 -1
- package/kanban/client/dist/assets/index-BfLDUxPS.js +0 -353
- package/kanban/client/dist/assets/index-C7W_e4ik.css +0 -1
package/README.md
CHANGED
|
@@ -384,25 +384,95 @@ The Seed button appears on story cards in the kanban board detail modal. It runs
|
|
|
384
384
|
|
|
385
385
|
### Run (Kanban Run Button)
|
|
386
386
|
|
|
387
|
-
Implements a task's code in an isolated git worktree using AI
|
|
387
|
+
Implements a task's code in an isolated git worktree using an **agentic tool-calling loop** — the same pattern used by Claude Code, OpenAI Codex, and Cursor. The AI agent iteratively reads existing code, writes files, runs tests, and fixes failures until all acceptance criteria pass. The worktree then moves to human review before merging.
|
|
388
|
+
|
|
389
|
+
#### Agentic Tool-Calling Loop
|
|
390
|
+
|
|
391
|
+
The Run ceremony follows the **Claude Code architecture**: a single-threaded loop where the model calls tools until it decides to stop. There is no fixed sequence — the model itself is the orchestrator.
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
while (model produces tool calls):
|
|
395
|
+
execute each tool call in the worktree sandbox
|
|
396
|
+
feed results back to model
|
|
397
|
+
model decides next action (or stops)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The loop ends when the model returns a text response with no tool calls — the same stop condition as Claude Code. A safety bound of 50 iterations prevents runaway loops.
|
|
401
|
+
|
|
402
|
+
#### Available Tools
|
|
403
|
+
|
|
404
|
+
The agent has 7 tools, all sandboxed to the worktree directory (paths outside the worktree are rejected):
|
|
405
|
+
|
|
406
|
+
| Tool | Purpose | Example |
|
|
407
|
+
|------|---------|---------|
|
|
408
|
+
| `read_file(path)` | Read existing code | Inspect imports, patterns, structure before writing |
|
|
409
|
+
| `write_file(path, content)` | Create or overwrite files | Write new source files and tests |
|
|
410
|
+
| `edit_file(path, old, new)` | Targeted search-and-replace | Fix a specific function without rewriting the whole file |
|
|
411
|
+
| `delete_file(path)` | Remove files | Clean up obsolete code |
|
|
412
|
+
| `list_files(pattern?)` | Explore project structure | `list_files("src/**/*.js")` to see existing code |
|
|
413
|
+
| `run_command(cmd)` | Execute shell commands | `npm test`, `npm install`, `npx tsc` |
|
|
414
|
+
| `search_code(pattern, glob?)` | Search for patterns in code | Find function definitions, import usages |
|
|
415
|
+
|
|
416
|
+
#### Typical Agent Workflow
|
|
417
|
+
|
|
418
|
+
The agent follows an explore-implement-test-fix cycle:
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
Agent: list_files("**/*") # explore project structure
|
|
422
|
+
Agent: read_file("src/index.html") # understand existing code
|
|
423
|
+
Agent: read_file("package.json") # check test setup
|
|
424
|
+
Agent: write_file("src/calculator.js", code) # implement the feature
|
|
425
|
+
Agent: write_file("test/calculator.test.js", tests) # write tests
|
|
426
|
+
Agent: run_command("npm test") # run tests
|
|
427
|
+
-> "FAIL: expected 8 but got NaN"
|
|
428
|
+
Agent: read_file("src/calculator.js") # inspect the failure
|
|
429
|
+
Agent: edit_file("src/calculator.js", # targeted fix
|
|
430
|
+
"parseInt(a)", "Number(a)")
|
|
431
|
+
Agent: run_command("npm test") # re-run
|
|
432
|
+
-> "PASS: 5 tests passed"
|
|
433
|
+
Agent: "Done. All 5 tests pass." # stops (no more tool calls)
|
|
434
|
+
```
|
|
388
435
|
|
|
389
436
|
#### Worktree Lifecycle
|
|
390
437
|
|
|
391
|
-
1. **Pre-flight check** — verifies
|
|
392
|
-
2. **Create worktree** — `git worktree add .avc/worktrees/{taskId} -b avc/{taskId}`
|
|
393
|
-
3. **Read documentation chain** —
|
|
394
|
-
4. **
|
|
395
|
-
5. **
|
|
396
|
-
6. **
|
|
397
|
-
|
|
438
|
+
1. **Pre-flight check** — verifies git repo exists.
|
|
439
|
+
2. **Create worktree** — `git worktree add .avc/worktrees/{taskId} -b avc/{taskId}`. Excludes `.avc/`, `node_modules/`, and `.env` from the worktree.
|
|
440
|
+
3. **Read documentation chain** — concatenates all `doc.md` and `context.md` files from project -> epic -> story -> task into a single context string.
|
|
441
|
+
4. **Agentic loop** — the agent receives the doc chain, acceptance criteria, and tools. It implements the code iteratively until all tests pass.
|
|
442
|
+
5. **Commit in worktree** — stages and commits all changes. Does NOT merge to main.
|
|
443
|
+
6. **Move to Review** — task status becomes `implemented` (Review column). The worktree stays on disk for human inspection.
|
|
444
|
+
|
|
445
|
+
#### Human Review
|
|
446
|
+
|
|
447
|
+
After the agent completes, the task appears in the **Review** column on the kanban board. The reviewer can:
|
|
448
|
+
|
|
449
|
+
- Inspect the code: `cd .avc/worktrees/{taskId}`
|
|
450
|
+
- Run tests: `npm test`
|
|
451
|
+
- Review the diff: `git log --oneline -1 && git diff HEAD~1`
|
|
452
|
+
- **Approve** — move to `completed` (automatically merges the branch to main and cleans up the worktree)
|
|
453
|
+
- **Reject** — move to `failed` (can retry with Run button)
|
|
398
454
|
|
|
399
|
-
####
|
|
455
|
+
#### Comparison with Other AI Coding Tools
|
|
400
456
|
|
|
401
|
-
|
|
457
|
+
AVC's Run ceremony follows the same core architecture as industry-leading AI coding tools, adapted for structured project contexts:
|
|
458
|
+
|
|
459
|
+
| Aspect | AVC Run | Claude Code | OpenAI Codex | Cursor |
|
|
460
|
+
|--------|---------|------------|-------------|--------|
|
|
461
|
+
| **Loop pattern** | Tool-calling loop, model stops when done | Same | Same | Same (ReAct) |
|
|
462
|
+
| **# of tools** | 7 (scoped to worktree) | 25+ (general purpose) | 5 + MCP | 10+ |
|
|
463
|
+
| **Sandbox** | Git worktree (disposable) | Permission system | OS-native (Bubblewrap) | Custom infra |
|
|
464
|
+
| **Context** | Pre-scoped doc chain (project->task) | CLAUDE.md + exploration | AGENTS.md + exploration | Trained embeddings |
|
|
465
|
+
| **Test handling** | Agent runs tests via `run_command`, reads failures, fixes iteratively | Same pattern | Same pattern | Same pattern |
|
|
466
|
+
| **Review** | Human review gate before merge | Immediate (user decides) | Immediate | Immediate |
|
|
467
|
+
| **Subagents** | No (tasks are small by design) | Yes (depth-limited) | Yes (Guardian) | Yes (8 parallel) |
|
|
468
|
+
|
|
469
|
+
**Key difference**: AVC provides pre-scoped context via the hierarchical documentation chain (project -> epic -> story -> task `context.md` + `doc.md`), so the agent doesn't need to explore the codebase from scratch. Claude Code and Codex must discover project structure themselves via tool calls. This makes AVC's loop shorter and cheaper — typically 10-20 tool calls vs 30-50 for a comparable task in Claude Code.
|
|
470
|
+
|
|
471
|
+
**Why fewer tools**: AVC's agent implements one atomic task (2-5 acceptance criteria) in an isolated worktree. It doesn't need web search (docs are in the context chain), task management (handled by work.json), user questions (specs are complete from Seed/Sprint Planning), or subagents (tasks are small enough for one context window).
|
|
402
472
|
|
|
403
473
|
#### Configuration
|
|
404
474
|
|
|
405
|
-
The Run ceremony is configured in `.avc/avc.json` with per-stage model selection (`code-generation`, `code-validation`)
|
|
475
|
+
The Run ceremony is configured in `.avc/avc.json` with per-stage model selection (`code-generation`, `code-validation`). Code check definitions in `src/cli/checks/code/` are customizable via the kanban Settings -> Agents tab.
|
|
406
476
|
|
|
407
477
|
### Kanban Board (`/kanban`)
|
|
408
478
|
|
|
@@ -465,7 +535,7 @@ AVC uses a **single prompt strategy** where agent instructions and task data are
|
|
|
465
535
|
|
|
466
536
|
## Code Generation Rules
|
|
467
537
|
|
|
468
|
-
AVC enforces coding rules on AI-generated code to ensure full traceability from requirements to implementation. These rules are
|
|
538
|
+
AVC enforces coding rules on AI-generated code to ensure full traceability from requirements to implementation. These rules are embedded in the [`code-implementer.md`](src/cli/agents/code-implementer.md) agent prompt (which the model follows during the agentic loop) and validated by check definitions in [`src/cli/checks/code/`](src/cli/checks/code/).
|
|
469
539
|
|
|
470
540
|
### Hierarchy-Prefixed Naming
|
|
471
541
|
|
|
@@ -507,3 +577,7 @@ Function names use vocabulary from the documentation chain — domain nouns and
|
|
|
507
577
|
4. **LLM Limitations Research** — [arXiv:2410.12972](https://arxiv.org/html/2410.12972v2) — 40-80% performance drops when combining knowledge and instruction-following in separate channels.
|
|
508
578
|
|
|
509
579
|
5. **Hierarchical Documentation Strategy** — Gloaguen et al. (2025) — [arXiv:2602.11988](https://arxiv.org/abs/2602.11988) — Comprehensive context files reduce agent task-success rates by 20%+.
|
|
580
|
+
|
|
581
|
+
6. **Claude Code Agent Architecture** — [How Claude Code Works](https://code.claude.com/docs/en/how-claude-code-works) — Single-threaded tool-calling loop where the model is the orchestrator. AVC's Run ceremony follows this same pattern.
|
|
582
|
+
|
|
583
|
+
7. **OpenAI Codex Agent Loop** — [Unrolling the Codex Agent Loop](https://openai.com/index/unrolling-the-codex-agent-loop/) — Tool-calling loop with sandboxed execution, same core architecture as Claude Code and AVC.
|
|
@@ -1,53 +1,39 @@
|
|
|
1
1
|
# Code Implementer Agent
|
|
2
2
|
|
|
3
|
-
You are an expert software engineer
|
|
3
|
+
You are an expert software engineer implementing a task in an isolated git worktree. You have tools to read, write, and edit files, run shell commands, and search code. Work iteratively — read existing code, write your implementation, run tests, and fix issues until all acceptance criteria pass.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Available Tools
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You have these tools. Use them as needed — there is no fixed order:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **read_file(path)** — Read a file's contents. Use to inspect existing code, imports, patterns.
|
|
10
|
+
- **write_file(path, content)** — Create or overwrite a file. Use for new source files and tests.
|
|
11
|
+
- **edit_file(path, old_string, new_string)** — Replace a specific string in a file. Use for targeted changes to existing files. The old_string must be unique and match exactly.
|
|
12
|
+
- **delete_file(path)** — Delete a file. Use to remove obsolete or incorrect files.
|
|
13
|
+
- **list_files(pattern?)** — List files matching a glob pattern. Use to explore the project structure.
|
|
14
|
+
- **run_command(command)** — Execute a shell command. Use for `npm test`, `npm install`, `npx tsc`, etc.
|
|
15
|
+
- **search_code(pattern, glob?)** — Search for a regex pattern in files. Use to find function definitions, imports, usages.
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
Follow this pattern, but adapt as needed:
|
|
20
|
+
|
|
21
|
+
1. **Explore** — Start by listing existing files and reading key files to understand the project structure, conventions, and existing code.
|
|
22
|
+
2. **Plan** — Based on the acceptance criteria and existing code, decide what files to create/edit and what functions to write.
|
|
23
|
+
3. **Implement** — Write source files following all coding rules below. Write test files that cover every acceptance criterion.
|
|
24
|
+
4. **Test** — Run the project's test command (typically `npm test`). Read the output.
|
|
25
|
+
5. **Fix** — If tests fail, read the error output, inspect the relevant code, and fix the issue. Repeat until tests pass.
|
|
26
|
+
6. **Verify** — Once tests pass, review your implementation to ensure all ACs are covered and all coding rules are followed.
|
|
27
|
+
|
|
28
|
+
When all tests pass and all acceptance criteria are satisfied, stop calling tools. Your final text response should be a brief summary of what was implemented.
|
|
29
|
+
|
|
30
|
+
## Input
|
|
10
31
|
|
|
11
32
|
You receive:
|
|
12
33
|
- `## Hierarchy Prefix` — the naming prefix for this task (e.g., `e0001_s0002_t0003`)
|
|
13
34
|
- `## Task ID` — the full task ID (e.g., `context-0001-0002-0003`)
|
|
14
35
|
- `## Acceptance Criteria` — numbered list of ACs from work.json
|
|
15
36
|
- `## Documentation Chain` — concatenated doc.md + context.md from project through task
|
|
16
|
-
- `## Coding Rules Summary` — the rules your code must follow
|
|
17
|
-
|
|
18
|
-
## Output Format
|
|
19
|
-
|
|
20
|
-
Return ONLY valid JSON:
|
|
21
|
-
|
|
22
|
-
```json
|
|
23
|
-
{
|
|
24
|
-
"files": [
|
|
25
|
-
{
|
|
26
|
-
"path": "src/domain/e0001-s0002-t0003-function-name.js",
|
|
27
|
-
"content": "full file content with provenance header and JSDoc",
|
|
28
|
-
"functions": ["e0001_s0002_t0003_functionName"]
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
"tests": [
|
|
32
|
-
{
|
|
33
|
-
"path": "src/domain/e0001-s0002-t0003-function-name.test.js",
|
|
34
|
-
"content": "full test file content",
|
|
35
|
-
"acceptanceCriteria": ["context-0001-0002-0003#AC1"]
|
|
36
|
-
}
|
|
37
|
-
],
|
|
38
|
-
"functionRegistry": [
|
|
39
|
-
{
|
|
40
|
-
"name": "e0001_s0002_t0003_functionName",
|
|
41
|
-
"file": "src/domain/e0001-s0002-t0003-function-name.js",
|
|
42
|
-
"type": "exported",
|
|
43
|
-
"pure": true,
|
|
44
|
-
"satisfies": "context-0001-0002-0003#AC1",
|
|
45
|
-
"lines": 18
|
|
46
|
-
}
|
|
47
|
-
],
|
|
48
|
-
"summary": "Brief description of what was implemented"
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
37
|
|
|
52
38
|
## Coding Rules
|
|
53
39
|
|
|
@@ -90,7 +76,6 @@ Every exported function has:
|
|
|
90
76
|
### 5. Functional Purity
|
|
91
77
|
- Business logic functions must be pure (same input → same output, no side effects)
|
|
92
78
|
- Side effects (file I/O, API calls, database) go in separate boundary functions
|
|
93
|
-
- Mark pure functions with `pure: true` in the function registry
|
|
94
79
|
|
|
95
80
|
### 6. Domain Naming
|
|
96
81
|
- Use nouns and verbs from the documentation chain
|
|
@@ -109,9 +94,11 @@ Every exported function has:
|
|
|
109
94
|
|
|
110
95
|
## Important Rules
|
|
111
96
|
|
|
112
|
-
1. **
|
|
113
|
-
2. **
|
|
114
|
-
3. **
|
|
115
|
-
4. **
|
|
116
|
-
5. **
|
|
117
|
-
6. **
|
|
97
|
+
1. **Start by exploring** — Always read existing files before writing. Understand the project structure, existing patterns, and imports.
|
|
98
|
+
2. **Follow the tech stack** from the project documentation exactly — do not introduce new frameworks or libraries unless the documentation specifies them.
|
|
99
|
+
3. **Implement ONLY what the task requires** — no extra features, no premature abstractions.
|
|
100
|
+
4. **Every function must trace to an AC** — no orphan code.
|
|
101
|
+
5. **Use the exact hierarchy prefix provided** — do not invent your own.
|
|
102
|
+
6. **Run tests after writing code** — do not stop without verifying.
|
|
103
|
+
7. **Fix test failures** — read the error, find the bug, fix it, re-run. Do not give up after one failure.
|
|
104
|
+
8. **When done, stop calling tools** — just return a text summary of what you implemented.
|
package/cli/init.js
CHANGED
|
@@ -957,11 +957,12 @@ class ProjectInitiator {
|
|
|
957
957
|
// Items to add to gitignore
|
|
958
958
|
const itemsToIgnore = [
|
|
959
959
|
{ pattern: '.env', comment: 'Environment variables' },
|
|
960
|
-
{ pattern: '.avc/documentation/.vitepress/dist', comment: 'VitePress build output' },
|
|
961
|
-
{ pattern: '.avc/documentation/.vitepress/cache', comment: 'VitePress cache' },
|
|
962
960
|
{ pattern: '.avc/logs', comment: 'Command execution logs' },
|
|
963
961
|
{ pattern: '.avc/token-history.json', comment: 'Token usage tracking' },
|
|
964
|
-
{ pattern: '.avc/ceremonies-history.json', comment: 'Ceremony execution history' }
|
|
962
|
+
{ pattern: '.avc/ceremonies-history.json', comment: 'Ceremony execution history' },
|
|
963
|
+
{ pattern: '.avc/worktrees', comment: 'AVC worktrees (generated code branches)' },
|
|
964
|
+
{ pattern: '.avc/documentation/.vitepress/dist', comment: 'VitePress build output' },
|
|
965
|
+
{ pattern: '.avc/documentation/.vitepress/cache', comment: 'VitePress cache' },
|
|
965
966
|
];
|
|
966
967
|
|
|
967
968
|
let newContent = gitignoreContent;
|
package/cli/llm-claude.js
CHANGED
|
@@ -173,4 +173,76 @@ export class ClaudeProvider extends LLMProvider {
|
|
|
173
173
|
});
|
|
174
174
|
return text;
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Multi-turn conversation with tool calling for the agentic loop.
|
|
179
|
+
* Uses Anthropic's messages API with tool_use/tool_result content blocks.
|
|
180
|
+
*/
|
|
181
|
+
async chat(messages, options = {}) {
|
|
182
|
+
if (!this._client) this._client = this._createClient();
|
|
183
|
+
|
|
184
|
+
const maxTokens = options.maxTokens || getMaxTokensForModel(this.model);
|
|
185
|
+
|
|
186
|
+
// Convert unified message format to Anthropic format
|
|
187
|
+
const anthropicMessages = [];
|
|
188
|
+
for (const msg of messages) {
|
|
189
|
+
if (msg.role === 'system') continue; // handled via system param
|
|
190
|
+
if (msg.role === 'tool') {
|
|
191
|
+
// Tool results go as user messages with tool_result content blocks
|
|
192
|
+
anthropicMessages.push({
|
|
193
|
+
role: 'user',
|
|
194
|
+
content: [{ type: 'tool_result', tool_use_id: msg.toolCallId, content: msg.content }],
|
|
195
|
+
});
|
|
196
|
+
} else if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
197
|
+
// Assistant messages with tool calls become tool_use content blocks
|
|
198
|
+
const content = [];
|
|
199
|
+
if (msg.content) content.push({ type: 'text', text: msg.content });
|
|
200
|
+
for (const tc of msg.toolCalls) {
|
|
201
|
+
content.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.arguments });
|
|
202
|
+
}
|
|
203
|
+
anthropicMessages.push({ role: 'assistant', content });
|
|
204
|
+
} else {
|
|
205
|
+
anthropicMessages.push({ role: msg.role, content: msg.content });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Convert tools from OpenAI format to Anthropic format
|
|
210
|
+
const anthropicTools = (options.tools || []).map(t => ({
|
|
211
|
+
name: t.function.name,
|
|
212
|
+
description: t.function.description,
|
|
213
|
+
input_schema: t.function.parameters,
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
const params = {
|
|
217
|
+
model: this.model,
|
|
218
|
+
max_tokens: maxTokens,
|
|
219
|
+
messages: anthropicMessages,
|
|
220
|
+
};
|
|
221
|
+
if (anthropicTools.length > 0) params.tools = anthropicTools;
|
|
222
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
223
|
+
if (systemMsg) params.system = systemMsg.content;
|
|
224
|
+
if (options.system) params.system = options.system;
|
|
225
|
+
|
|
226
|
+
const response = await this._withRetry(
|
|
227
|
+
() => this._client.messages.create(params),
|
|
228
|
+
'Chat (Claude)'
|
|
229
|
+
);
|
|
230
|
+
this._trackTokens(response.usage);
|
|
231
|
+
|
|
232
|
+
// Extract text and tool calls from response content blocks
|
|
233
|
+
let textContent = '';
|
|
234
|
+
const toolCalls = [];
|
|
235
|
+
for (const block of response.content) {
|
|
236
|
+
if (block.type === 'text') textContent += block.text;
|
|
237
|
+
if (block.type === 'tool_use') {
|
|
238
|
+
toolCalls.push({ id: block.id, name: block.name, arguments: block.input });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
content: textContent,
|
|
244
|
+
toolCalls,
|
|
245
|
+
stopReason: response.stop_reason, // 'end_turn' | 'tool_use'
|
|
246
|
+
};
|
|
247
|
+
}
|
|
176
248
|
}
|
package/cli/llm-gemini.js
CHANGED
|
@@ -136,4 +136,80 @@ export class GeminiProvider extends LLMProvider {
|
|
|
136
136
|
});
|
|
137
137
|
return text;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Multi-turn conversation with tool calling for the agentic loop.
|
|
142
|
+
* Uses Gemini's generateContent with functionDeclarations.
|
|
143
|
+
*/
|
|
144
|
+
async chat(messages, options = {}) {
|
|
145
|
+
if (!this._client) this._client = this._createClient();
|
|
146
|
+
|
|
147
|
+
// Convert messages to Gemini format
|
|
148
|
+
const contents = [];
|
|
149
|
+
for (const msg of messages) {
|
|
150
|
+
if (msg.role === 'system') continue;
|
|
151
|
+
if (msg.role === 'tool') {
|
|
152
|
+
contents.push({
|
|
153
|
+
role: 'function',
|
|
154
|
+
parts: [{ functionResponse: { name: msg.toolName || 'tool', response: { result: msg.content } } }],
|
|
155
|
+
});
|
|
156
|
+
} else if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
157
|
+
const parts = [];
|
|
158
|
+
if (msg.content) parts.push({ text: msg.content });
|
|
159
|
+
for (const tc of msg.toolCalls) {
|
|
160
|
+
parts.push({ functionCall: { name: tc.name, args: tc.arguments } });
|
|
161
|
+
}
|
|
162
|
+
contents.push({ role: 'model', parts });
|
|
163
|
+
} else {
|
|
164
|
+
contents.push({ role: msg.role === 'assistant' ? 'model' : 'user', parts: [{ text: msg.content }] });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Convert tools from OpenAI format to Gemini format
|
|
169
|
+
const geminiTools = (options.tools || []).length > 0
|
|
170
|
+
? [{ functionDeclarations: options.tools.map(t => ({
|
|
171
|
+
name: t.function.name,
|
|
172
|
+
description: t.function.description,
|
|
173
|
+
parameters: t.function.parameters,
|
|
174
|
+
})) }]
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
177
|
+
const params = {
|
|
178
|
+
model: this.model,
|
|
179
|
+
contents,
|
|
180
|
+
};
|
|
181
|
+
const config = {};
|
|
182
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
183
|
+
if (systemMsg || options.system) config.systemInstruction = systemMsg?.content || options.system;
|
|
184
|
+
if (geminiTools) config.tools = geminiTools;
|
|
185
|
+
if (Object.keys(config).length > 0) params.config = config;
|
|
186
|
+
|
|
187
|
+
const response = await this._withRetry(
|
|
188
|
+
() => this._client.models.generateContent(params),
|
|
189
|
+
'Chat (Gemini)'
|
|
190
|
+
);
|
|
191
|
+
this._trackTokens(response.usageMetadata);
|
|
192
|
+
|
|
193
|
+
// Extract text and function calls from response
|
|
194
|
+
let textContent = '';
|
|
195
|
+
const toolCalls = [];
|
|
196
|
+
const parts = response.candidates?.[0]?.content?.parts || [];
|
|
197
|
+
for (const part of parts) {
|
|
198
|
+
if (part.text) textContent += part.text;
|
|
199
|
+
if (part.functionCall) {
|
|
200
|
+
toolCalls.push({
|
|
201
|
+
id: `gemini-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
202
|
+
name: part.functionCall.name,
|
|
203
|
+
arguments: part.functionCall.args || {},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const finishReason = response.candidates?.[0]?.finishReason || 'STOP';
|
|
209
|
+
return {
|
|
210
|
+
content: textContent,
|
|
211
|
+
toolCalls,
|
|
212
|
+
stopReason: finishReason, // 'STOP' | 'TOOL_CALLS'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
139
215
|
}
|
package/cli/llm-local.js
CHANGED
|
@@ -490,4 +490,56 @@ export class LocalProvider extends LLMProvider {
|
|
|
490
490
|
});
|
|
491
491
|
return textContent;
|
|
492
492
|
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Multi-turn conversation with tool calling (OpenAI-compatible API).
|
|
496
|
+
* Local models have variable tool-calling support — falls back gracefully.
|
|
497
|
+
*/
|
|
498
|
+
async chat(messages, options = {}) {
|
|
499
|
+
if (!this._client) this._client = this._createClient();
|
|
500
|
+
|
|
501
|
+
const openaiMessages = messages.map(msg => {
|
|
502
|
+
if (msg.role === 'tool') {
|
|
503
|
+
return { role: 'tool', tool_call_id: msg.toolCallId, content: msg.content };
|
|
504
|
+
}
|
|
505
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
506
|
+
return {
|
|
507
|
+
role: 'assistant',
|
|
508
|
+
content: msg.content || null,
|
|
509
|
+
tool_calls: msg.toolCalls.map(tc => ({
|
|
510
|
+
id: tc.id,
|
|
511
|
+
type: 'function',
|
|
512
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
|
|
513
|
+
})),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return { role: msg.role, content: msg.content };
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const params = {
|
|
520
|
+
model: this.model,
|
|
521
|
+
messages: openaiMessages,
|
|
522
|
+
};
|
|
523
|
+
if (options.tools?.length > 0) params.tools = options.tools;
|
|
524
|
+
if (options.maxTokens) params.max_tokens = options.maxTokens;
|
|
525
|
+
|
|
526
|
+
const response = await this._withRetry(
|
|
527
|
+
() => this._client.chat.completions.create(params),
|
|
528
|
+
'Chat (Local)'
|
|
529
|
+
);
|
|
530
|
+
this._trackTokens(response.usage);
|
|
531
|
+
|
|
532
|
+
const choice = response.choices[0];
|
|
533
|
+
const toolCalls = (choice.message.tool_calls || []).map(tc => ({
|
|
534
|
+
id: tc.id,
|
|
535
|
+
name: tc.function.name,
|
|
536
|
+
arguments: typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments,
|
|
537
|
+
}));
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
content: choice.message.content || '',
|
|
541
|
+
toolCalls,
|
|
542
|
+
stopReason: choice.finish_reason,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
493
545
|
}
|
package/cli/llm-openai.js
CHANGED
|
@@ -451,4 +451,56 @@ export class OpenAIProvider extends LLMProvider {
|
|
|
451
451
|
return textContent;
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Multi-turn conversation with tool calling for the agentic loop.
|
|
457
|
+
* Uses OpenAI's chat.completions API with function calling.
|
|
458
|
+
*/
|
|
459
|
+
async chat(messages, options = {}) {
|
|
460
|
+
if (!this._client) this._client = this._createClient();
|
|
461
|
+
|
|
462
|
+
const openaiMessages = messages.map(msg => {
|
|
463
|
+
if (msg.role === 'tool') {
|
|
464
|
+
return { role: 'tool', tool_call_id: msg.toolCallId, content: msg.content };
|
|
465
|
+
}
|
|
466
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
467
|
+
return {
|
|
468
|
+
role: 'assistant',
|
|
469
|
+
content: msg.content || null,
|
|
470
|
+
tool_calls: msg.toolCalls.map(tc => ({
|
|
471
|
+
id: tc.id,
|
|
472
|
+
type: 'function',
|
|
473
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
|
|
474
|
+
})),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return { role: msg.role, content: msg.content };
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const params = {
|
|
481
|
+
model: this.model,
|
|
482
|
+
messages: openaiMessages,
|
|
483
|
+
};
|
|
484
|
+
if (options.tools?.length > 0) params.tools = options.tools;
|
|
485
|
+
if (options.maxTokens) params.max_tokens = options.maxTokens;
|
|
486
|
+
|
|
487
|
+
const response = await this._withRetry(
|
|
488
|
+
() => this._client.chat.completions.create(params),
|
|
489
|
+
'Chat (OpenAI)'
|
|
490
|
+
);
|
|
491
|
+
this._trackTokens(response.usage);
|
|
492
|
+
|
|
493
|
+
const choice = response.choices[0];
|
|
494
|
+
const toolCalls = (choice.message.tool_calls || []).map(tc => ({
|
|
495
|
+
id: tc.id,
|
|
496
|
+
name: tc.function.name,
|
|
497
|
+
arguments: JSON.parse(tc.function.arguments),
|
|
498
|
+
}));
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
content: choice.message.content || '',
|
|
502
|
+
toolCalls,
|
|
503
|
+
stopReason: choice.finish_reason, // 'stop' | 'tool_calls'
|
|
504
|
+
};
|
|
505
|
+
}
|
|
454
506
|
}
|
package/cli/llm-provider.js
CHANGED
|
@@ -433,6 +433,18 @@ export class LLMProvider {
|
|
|
433
433
|
throw new Error(`${this.constructor.name} must implement generateJSON()`);
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Multi-turn conversation with tool calling — the agentic loop primitive.
|
|
438
|
+
* Sends messages + tool definitions, returns the model's response including any tool calls.
|
|
439
|
+
*
|
|
440
|
+
* @param {Array} messages - Conversation history [{role, content, toolCalls?, toolCallId?}]
|
|
441
|
+
* @param {object} options - { tools?, maxTokens?, system? }
|
|
442
|
+
* @returns {Promise<{ content: string, toolCalls: Array<{id, name, arguments}>, stopReason: string }>}
|
|
443
|
+
*/
|
|
444
|
+
async chat(messages, options = {}) {
|
|
445
|
+
throw new Error(`${this.constructor.name} must implement chat()`);
|
|
446
|
+
}
|
|
447
|
+
|
|
436
448
|
// Subclass hooks — throw if not overridden
|
|
437
449
|
_createClient() { throw new Error(`${this.constructor.name} must implement _createClient()`); }
|
|
438
450
|
async _callProvider(prompt, maxTokens, systemInstructions) { throw new Error(`${this.constructor.name} must implement _callProvider()`); }
|
package/cli/llm-xiaomi.js
CHANGED
|
@@ -140,4 +140,55 @@ export class XiaomiProvider extends LLMProvider {
|
|
|
140
140
|
|
|
141
141
|
return content;
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Multi-turn conversation with tool calling (OpenAI-compatible API).
|
|
146
|
+
*/
|
|
147
|
+
async chat(messages, options = {}) {
|
|
148
|
+
if (!this._client) this._client = this._createClient();
|
|
149
|
+
|
|
150
|
+
const openaiMessages = messages.map(msg => {
|
|
151
|
+
if (msg.role === 'tool') {
|
|
152
|
+
return { role: 'tool', tool_call_id: msg.toolCallId, content: msg.content };
|
|
153
|
+
}
|
|
154
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
155
|
+
return {
|
|
156
|
+
role: 'assistant',
|
|
157
|
+
content: msg.content || null,
|
|
158
|
+
tool_calls: msg.toolCalls.map(tc => ({
|
|
159
|
+
id: tc.id,
|
|
160
|
+
type: 'function',
|
|
161
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { role: msg.role, content: msg.content };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const params = {
|
|
169
|
+
model: this.model,
|
|
170
|
+
messages: openaiMessages,
|
|
171
|
+
};
|
|
172
|
+
if (options.tools?.length > 0) params.tools = options.tools;
|
|
173
|
+
if (options.maxTokens) params.max_tokens = options.maxTokens;
|
|
174
|
+
|
|
175
|
+
const response = await this._withRetry(
|
|
176
|
+
() => this._client.chat.completions.create(params),
|
|
177
|
+
'Chat (Xiaomi)'
|
|
178
|
+
);
|
|
179
|
+
this._trackTokens(response.usage);
|
|
180
|
+
|
|
181
|
+
const choice = response.choices[0];
|
|
182
|
+
const toolCalls = (choice.message.tool_calls || []).map(tc => ({
|
|
183
|
+
id: tc.id,
|
|
184
|
+
name: tc.function.name,
|
|
185
|
+
arguments: JSON.parse(tc.function.arguments),
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
content: choice.message.content || '',
|
|
190
|
+
toolCalls,
|
|
191
|
+
stopReason: choice.finish_reason,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
143
194
|
}
|
package/cli/seed-processor.js
CHANGED
|
@@ -779,6 +779,37 @@ Return your response as JSON following the exact structure specified in your ins
|
|
|
779
779
|
this.debug(`[INFO] Updated story doc.md after task extraction (${storyDocContent.length} bytes)`);
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
+
// Compute ready status for tasks/subtasks with no pending dependencies
|
|
783
|
+
try {
|
|
784
|
+
const { checkDependenciesReady } = await import('./dependency-checker.js');
|
|
785
|
+
const lookup = {};
|
|
786
|
+
for (const task of hierarchy.tasks) {
|
|
787
|
+
lookup[task.id] = { id: task.id, name: task.name, type: 'task', status: 'planned', dependencies: task.dependencies || [] };
|
|
788
|
+
for (const sub of task.subtasks || []) {
|
|
789
|
+
lookup[sub.id] = { id: sub.id, name: sub.name, type: 'subtask', status: 'planned', dependencies: sub.dependencies || [] };
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
let readyCount = 0;
|
|
794
|
+
for (const id of Object.keys(lookup)) {
|
|
795
|
+
const result = checkDependenciesReady(id, lookup);
|
|
796
|
+
if (result.ready) {
|
|
797
|
+
const itemDir = path.join(this.projectPath, id);
|
|
798
|
+
const wjPath = path.join(itemDir, 'work.json');
|
|
799
|
+
if (fs.existsSync(wjPath)) {
|
|
800
|
+
const wj = JSON.parse(fs.readFileSync(wjPath, 'utf8'));
|
|
801
|
+
wj.status = 'ready';
|
|
802
|
+
fs.writeFileSync(wjPath, JSON.stringify(wj, null, 2), 'utf8');
|
|
803
|
+
lookup[id].status = 'ready';
|
|
804
|
+
readyCount++;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
this.debug(`[INFO] Set ${readyCount} task/subtask items to 'ready' status (dependency-free)`);
|
|
809
|
+
} catch (err) {
|
|
810
|
+
this.debug(`[WARN] Ready status computation failed (non-critical): ${err.message}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
782
813
|
return { taskCount, subtaskCount, taskIds };
|
|
783
814
|
}
|
|
784
815
|
|