@cephalization/math 0.2.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 +190 -0
- package/index.ts +155 -0
- package/package.json +50 -0
- package/src/agent.test.ts +179 -0
- package/src/agent.ts +275 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/iterate.ts +92 -0
- package/src/commands/plan.ts +16 -0
- package/src/commands/prune.ts +63 -0
- package/src/commands/run.test.ts +27 -0
- package/src/commands/run.ts +16 -0
- package/src/commands/status.ts +55 -0
- package/src/constants.ts +1 -0
- package/src/loop.test.ts +537 -0
- package/src/loop.ts +325 -0
- package/src/plan.ts +263 -0
- package/src/prune.test.ts +174 -0
- package/src/prune.ts +146 -0
- package/src/tasks.ts +204 -0
- package/src/templates.ts +172 -0
- package/src/ui/app.test.ts +228 -0
- package/src/ui/buffer.test.ts +222 -0
- package/src/ui/buffer.ts +131 -0
- package/src/ui/server.test.ts +271 -0
- package/src/ui/server.ts +124 -0
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# math - Multi-Agent Todo Harness
|
|
2
|
+
|
|
3
|
+
A light meta agent orchestration harness designed to coordinate multiple AI agents working together to accomplish tasks from a TODO list.
|
|
4
|
+
|
|
5
|
+
## Core Concept
|
|
6
|
+
|
|
7
|
+
The primary responsibility of this harness is to **reduce context bloat** by digesting a project plan into three documents:
|
|
8
|
+
|
|
9
|
+
| Document | Purpose |
|
|
10
|
+
| ---------- | --------- |
|
|
11
|
+
| `TASKS.md` | Task list with status tracking and dependencies |
|
|
12
|
+
| `LEARNINGS.md` | Accumulated insights from completed tasks |
|
|
13
|
+
| `PROMPT.md` | System prompt with guardrails ("signs") |
|
|
14
|
+
|
|
15
|
+
The harness consists of a simple for-loop, executing a new coding agent with a mandate from `PROMPT.md` to complete a *single* task from `TASKS.md`, while reading and recording any insight gained during the work into `LEARNINGS.md`.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
**[Bun](https://bun.sh) is required** to run this tool. Node.js is not supported.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Install Bun (macOS, Linux, WSL)
|
|
23
|
+
curl -fsSL https://bun.sh/install | bash
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Why Bun?
|
|
27
|
+
- This tool is written in TypeScript and uses Bun's native TypeScript execution (no compilation step)
|
|
28
|
+
- The CLI uses a `#!/usr/bin/env bun` shebang for direct execution
|
|
29
|
+
- Bun's speed makes the agent loop faster and more responsive
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
### From npm (recommended)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# One-off usage (recommended)
|
|
37
|
+
bunx @cephalization/math <command>
|
|
38
|
+
|
|
39
|
+
# Global install
|
|
40
|
+
bun install -g @cephalization/math
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### From source (for development)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/cephalization/math.git
|
|
47
|
+
cd math
|
|
48
|
+
bun install
|
|
49
|
+
bun link
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Initialize a project
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
math init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Creates a `todo/` directory with template files and offers to run **planning mode** to help you break down your goal into tasks.
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
|
|
64
|
+
- `--no-plan` - Skip the planning prompt
|
|
65
|
+
- `--model <model>` - Model to use (default: `anthropic/claude-opus-4-5`)
|
|
66
|
+
|
|
67
|
+
### Plan your tasks
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
math plan
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
|
|
75
|
+
- `--model <model>` - Model to use (default: `anthropic/claude-opus-4-5`)
|
|
76
|
+
- `--quick` - Skip clarifying questions and generate plan immediately
|
|
77
|
+
|
|
78
|
+
Interactively plan your tasks with AI assistance. The planner uses a two-phase approach:
|
|
79
|
+
|
|
80
|
+
1. **Clarification phase**: The AI analyzes your goal and asks 3-5 clarifying questions
|
|
81
|
+
2. **Planning phase**: Using your answers, it generates a well-structured task list
|
|
82
|
+
|
|
83
|
+
Use `--quick` to skip the clarification phase if you want a faster, assumption-based plan.
|
|
84
|
+
|
|
85
|
+
### Run the agent loop
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
math run
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
|
|
93
|
+
- `--model <model>` - Model to use (default: `anthropic/claude-opus-4-5`)
|
|
94
|
+
- `--max-iterations <n>` - Safety limit (default: 100)
|
|
95
|
+
- `--pause <seconds>` - Pause between iterations (default: 3)
|
|
96
|
+
|
|
97
|
+
### Check status
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
math status
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Shows task progress with a visual progress bar and next task info.
|
|
104
|
+
|
|
105
|
+
### Start a new sprint
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
math iterate
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Backs up `todo/` to `todo-{M}-{D}-{Y}/` and resets for a new goal:
|
|
112
|
+
|
|
113
|
+
- TASKS.md and LEARNINGS.md are reset to templates
|
|
114
|
+
- PROMPT.md is preserved (keeping your accumulated "signs")
|
|
115
|
+
- Offers to run planning mode for your new goal
|
|
116
|
+
|
|
117
|
+
Options:
|
|
118
|
+
|
|
119
|
+
- `--no-plan` - Skip the planning prompt
|
|
120
|
+
|
|
121
|
+
## Task Format
|
|
122
|
+
|
|
123
|
+
Tasks in `TASKS.md` follow this format:
|
|
124
|
+
|
|
125
|
+
```markdown
|
|
126
|
+
### task-id
|
|
127
|
+
|
|
128
|
+
- content: Description of what to implement
|
|
129
|
+
- status: pending | in_progress | complete
|
|
130
|
+
- dependencies: task-1, task-2
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## The Loop
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
137
|
+
│ math run (loop) │
|
|
138
|
+
│ ┌───────────────────────────────────────────────────────┐ │
|
|
139
|
+
│ │ 1. Check TASKS.md for pending tasks │ │
|
|
140
|
+
│ │ 2. If all complete → EXIT SUCCESS │ │
|
|
141
|
+
│ │ 3. Invoke agent with PROMPT.md + TASKS.md │ │
|
|
142
|
+
│ │ 4. Agent: pick task → implement → test → commit │ │
|
|
143
|
+
│ │ 5. Agent: update TASKS.md → log learnings → EXIT │ │
|
|
144
|
+
│ │ 6. Loop back to step 1 │ │
|
|
145
|
+
│ └───────────────────────────────────────────────────────┘ │
|
|
146
|
+
└─────────────────────────────────────────────────────────────┘
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Planning Mode
|
|
150
|
+
|
|
151
|
+
When you run `math init`, `math iterate`, or `math plan`, the harness can invoke [OpenCode](https://opencode.ai/docs/cli/) to help you plan:
|
|
152
|
+
|
|
153
|
+
1. You describe your high-level goal
|
|
154
|
+
2. OpenCode asks clarifying questions to understand your requirements
|
|
155
|
+
3. You answer the questions interactively
|
|
156
|
+
4. OpenCode breaks your goal into discrete, implementable tasks
|
|
157
|
+
5. Tasks are written to `TASKS.md` with proper dependencies
|
|
158
|
+
6. You're ready to run `math run`
|
|
159
|
+
|
|
160
|
+
This bridges the gap between "I want to build X" and a structured task list. The clarifying questions phase uses OpenCode's session continuation feature to maintain context across the conversation.
|
|
161
|
+
|
|
162
|
+
## Signs (Guardrails)
|
|
163
|
+
|
|
164
|
+
"Signs" are explicit guardrails in `PROMPT.md` that prevent common agent mistakes. Add new signs whenever you discover a failure mode:
|
|
165
|
+
|
|
166
|
+
```markdown
|
|
167
|
+
### SIGN: Descriptive Name
|
|
168
|
+
|
|
169
|
+
Clear explanation of what to do or avoid.
|
|
170
|
+
|
|
171
|
+
❌ WRONG: Example of the mistake
|
|
172
|
+
✅ RIGHT: Example of correct behavior
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Signs accumulate over time, making the agent increasingly reliable.
|
|
176
|
+
|
|
177
|
+
## Environment Variables
|
|
178
|
+
|
|
179
|
+
| Variable | Default | Description |
|
|
180
|
+
|----------|---------|-------------|
|
|
181
|
+
| `MODEL` | `anthropic/claude-opus-4-20250514` | Model to use |
|
|
182
|
+
|
|
183
|
+
## Credits
|
|
184
|
+
|
|
185
|
+
- **Ralph Methodology**: [Geoffrey Huntley](https://ghuntley.com/ralph/)
|
|
186
|
+
- **Agent Runtime**: [OpenCode](https://opencode.ai)
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { init } from "./src/commands/init";
|
|
4
|
+
import { run } from "./src/commands/run";
|
|
5
|
+
import { status } from "./src/commands/status";
|
|
6
|
+
import { iterate } from "./src/commands/iterate";
|
|
7
|
+
import { plan } from "./src/commands/plan";
|
|
8
|
+
import { prune } from "./src/commands/prune";
|
|
9
|
+
import { DEFAULT_MODEL } from "./src/constants";
|
|
10
|
+
|
|
11
|
+
// ANSI colors
|
|
12
|
+
const colors = {
|
|
13
|
+
reset: "\x1b[0m",
|
|
14
|
+
bold: "\x1b[1m",
|
|
15
|
+
dim: "\x1b[2m",
|
|
16
|
+
red: "\x1b[31m",
|
|
17
|
+
green: "\x1b[32m",
|
|
18
|
+
yellow: "\x1b[33m",
|
|
19
|
+
blue: "\x1b[34m",
|
|
20
|
+
cyan: "\x1b[36m",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`
|
|
25
|
+
${colors.bold}math${colors.reset} - Multi-Agent Todo Harness
|
|
26
|
+
|
|
27
|
+
A light meta agent orchestration harness designed to coordinate multiple AI
|
|
28
|
+
agents working together to accomplish tasks from a TODO list.
|
|
29
|
+
|
|
30
|
+
${colors.bold}USAGE${colors.reset}
|
|
31
|
+
math <command> [options]
|
|
32
|
+
|
|
33
|
+
${colors.bold}COMMANDS${colors.reset}
|
|
34
|
+
${colors.cyan}init${colors.reset} Create todo/ directory with template files
|
|
35
|
+
${colors.cyan}plan${colors.reset} Run planning mode to flesh out tasks
|
|
36
|
+
${colors.cyan}run${colors.reset} Start the agent loop until all tasks complete
|
|
37
|
+
${colors.cyan}status${colors.reset} Show current task counts
|
|
38
|
+
${colors.cyan}iterate${colors.reset} Backup todo/ and reset for a new sprint
|
|
39
|
+
${colors.cyan}prune${colors.reset} Delete backup artifacts (todo-M-D-Y directories)
|
|
40
|
+
${colors.cyan}help${colors.reset} Show this help message
|
|
41
|
+
|
|
42
|
+
${colors.bold}OPTIONS${colors.reset}
|
|
43
|
+
${colors.dim}--model <model>${colors.reset} Model to use (default: ${DEFAULT_MODEL})
|
|
44
|
+
${colors.dim}--max-iterations <n>${colors.reset} Safety limit (default: 100)
|
|
45
|
+
${colors.dim}--pause <seconds>${colors.reset} Pause between iterations (default: 3)
|
|
46
|
+
${colors.dim}--no-plan${colors.reset} Skip planning mode after init/iterate
|
|
47
|
+
${colors.dim}--ui${colors.reset} Enable web UI server (run command only)
|
|
48
|
+
${colors.dim}--quick${colors.reset} Skip clarifying questions in plan mode
|
|
49
|
+
${colors.dim}--force${colors.reset} Skip confirmation prompts (prune)
|
|
50
|
+
|
|
51
|
+
${colors.bold}EXAMPLES${colors.reset}
|
|
52
|
+
${colors.dim}# Initialize and plan a new project${colors.reset}
|
|
53
|
+
math init
|
|
54
|
+
|
|
55
|
+
${colors.dim}# Initialize without planning${colors.reset}
|
|
56
|
+
math init --no-plan
|
|
57
|
+
|
|
58
|
+
${colors.dim}# Run planning mode on existing todo/${colors.reset}
|
|
59
|
+
math plan
|
|
60
|
+
|
|
61
|
+
${colors.dim}# Quick planning without clarifying questions${colors.reset}
|
|
62
|
+
math plan --quick
|
|
63
|
+
|
|
64
|
+
${colors.dim}# Run the agent loop${colors.reset}
|
|
65
|
+
math run
|
|
66
|
+
|
|
67
|
+
${colors.dim}# Run with web UI${colors.reset}
|
|
68
|
+
math run --ui
|
|
69
|
+
|
|
70
|
+
${colors.dim}# Check task status${colors.reset}
|
|
71
|
+
math status
|
|
72
|
+
|
|
73
|
+
${colors.dim}# Start a new sprint (backup current, reset, plan)${colors.reset}
|
|
74
|
+
math iterate
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseArgs(args: string[]): Record<string, string | boolean> {
|
|
79
|
+
const parsed: Record<string, string | boolean> = {};
|
|
80
|
+
for (let i = 0; i < args.length; i++) {
|
|
81
|
+
const arg = args[i];
|
|
82
|
+
if (!arg) continue;
|
|
83
|
+
if (arg.startsWith("--")) {
|
|
84
|
+
const key = arg.slice(2);
|
|
85
|
+
const next = args[i + 1];
|
|
86
|
+
if (next && !next.startsWith("--")) {
|
|
87
|
+
parsed[key] = next;
|
|
88
|
+
i++;
|
|
89
|
+
} else {
|
|
90
|
+
parsed[key] = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
const [command, ...rest] = Bun.argv.slice(2);
|
|
99
|
+
const options = parseArgs(rest);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
switch (command) {
|
|
103
|
+
case "init":
|
|
104
|
+
await init({
|
|
105
|
+
skipPlan: !!options["no-plan"],
|
|
106
|
+
model: options.model as string,
|
|
107
|
+
});
|
|
108
|
+
break;
|
|
109
|
+
case "plan":
|
|
110
|
+
await plan({
|
|
111
|
+
model: options.model as string | undefined,
|
|
112
|
+
quick: !!options.quick,
|
|
113
|
+
});
|
|
114
|
+
break;
|
|
115
|
+
case "run":
|
|
116
|
+
await run(options);
|
|
117
|
+
break;
|
|
118
|
+
case "status":
|
|
119
|
+
await status();
|
|
120
|
+
break;
|
|
121
|
+
case "iterate":
|
|
122
|
+
await iterate({
|
|
123
|
+
skipPlan: !!options["no-plan"],
|
|
124
|
+
model: options.model as string,
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
case "prune":
|
|
128
|
+
await prune({
|
|
129
|
+
force: !!options.force,
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
case "help":
|
|
133
|
+
case "--help":
|
|
134
|
+
case "-h":
|
|
135
|
+
case undefined:
|
|
136
|
+
printHelp();
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
console.error(
|
|
140
|
+
`${colors.red}Unknown command: ${command}${colors.reset}`
|
|
141
|
+
);
|
|
142
|
+
printHelp();
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(
|
|
147
|
+
`${colors.red}Error: ${error instanceof Error ? error.message : error}${
|
|
148
|
+
colors.reset
|
|
149
|
+
}`
|
|
150
|
+
);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cephalization/math",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"author": "Tony Powell <powell.anthonyd@proton.me>",
|
|
5
|
+
"access": "public",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/cephalization/math"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/cephalization/math",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/cephalization/math/issues"
|
|
13
|
+
},
|
|
14
|
+
"description": "Multi-Agent Todo Harness - A light meta agent orchestration harness",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"math": "./index.ts"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.ts",
|
|
21
|
+
"src/**/*.ts",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "bun index.ts",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"changeset": "bunx changeset"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@changesets/cli": "^2.29.8",
|
|
32
|
+
"@types/bun": "latest",
|
|
33
|
+
"typescript": "^5"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"ai",
|
|
37
|
+
"agent",
|
|
38
|
+
"multi-agent",
|
|
39
|
+
"todo",
|
|
40
|
+
"orchestration",
|
|
41
|
+
"bun"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@types/react": "^19.2.8",
|
|
46
|
+
"@types/react-dom": "^19.2.3",
|
|
47
|
+
"react": "^19.2.3",
|
|
48
|
+
"react-dom": "^19.2.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
MockAgent,
|
|
4
|
+
createMockAgent,
|
|
5
|
+
createLogEntry,
|
|
6
|
+
createAgentOutput,
|
|
7
|
+
type LogCategory,
|
|
8
|
+
type AgentRunOptions,
|
|
9
|
+
} from "./agent";
|
|
10
|
+
|
|
11
|
+
describe("MockAgent", () => {
|
|
12
|
+
test("isAvailable returns true by default", async () => {
|
|
13
|
+
const agent = createMockAgent();
|
|
14
|
+
expect(await agent.isAvailable()).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("isAvailable returns false when configured", async () => {
|
|
18
|
+
const agent = createMockAgent({ available: false });
|
|
19
|
+
expect(await agent.isAvailable()).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("run returns default logs and output", async () => {
|
|
23
|
+
const agent = createMockAgent();
|
|
24
|
+
const options: AgentRunOptions = {
|
|
25
|
+
model: "test-model",
|
|
26
|
+
prompt: "test prompt",
|
|
27
|
+
files: ["file1.md", "file2.md"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const result = await agent.run(options);
|
|
31
|
+
|
|
32
|
+
expect(result.exitCode).toBe(0);
|
|
33
|
+
expect(result.logs).toHaveLength(2);
|
|
34
|
+
expect(result.logs[0]!.category).toBe("info");
|
|
35
|
+
expect(result.logs[0]!.message).toBe("Mock agent starting...");
|
|
36
|
+
expect(result.logs[1]!.category).toBe("success");
|
|
37
|
+
expect(result.logs[1]!.message).toBe("Mock agent completed");
|
|
38
|
+
expect(result.output).toHaveLength(1);
|
|
39
|
+
expect(result.output[0]!.text).toBe("Mock agent output\n");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("run uses configured logs", async () => {
|
|
43
|
+
const customLogs = [
|
|
44
|
+
{ category: "info" as LogCategory, message: "Custom log 1" },
|
|
45
|
+
{ category: "warning" as LogCategory, message: "Custom warning" },
|
|
46
|
+
{ category: "error" as LogCategory, message: "Custom error" },
|
|
47
|
+
];
|
|
48
|
+
const agent = createMockAgent({ logs: customLogs });
|
|
49
|
+
|
|
50
|
+
const result = await agent.run({
|
|
51
|
+
model: "test",
|
|
52
|
+
prompt: "test",
|
|
53
|
+
files: [],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.logs).toHaveLength(3);
|
|
57
|
+
expect(result.logs[0]!.message).toBe("Custom log 1");
|
|
58
|
+
expect(result.logs[1]!.message).toBe("Custom warning");
|
|
59
|
+
expect(result.logs[2]!.message).toBe("Custom error");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("run uses configured output", async () => {
|
|
63
|
+
const customOutput = ["Line 1\n", "Line 2\n", "Line 3\n"];
|
|
64
|
+
const agent = createMockAgent({ output: customOutput });
|
|
65
|
+
|
|
66
|
+
const result = await agent.run({
|
|
67
|
+
model: "test",
|
|
68
|
+
prompt: "test",
|
|
69
|
+
files: [],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.output).toHaveLength(3);
|
|
73
|
+
expect(result.output[0]!.text).toBe("Line 1\n");
|
|
74
|
+
expect(result.output[1]!.text).toBe("Line 2\n");
|
|
75
|
+
expect(result.output[2]!.text).toBe("Line 3\n");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("run uses configured exit code", async () => {
|
|
79
|
+
const agent = createMockAgent({ exitCode: 1 });
|
|
80
|
+
|
|
81
|
+
const result = await agent.run({
|
|
82
|
+
model: "test",
|
|
83
|
+
prompt: "test",
|
|
84
|
+
files: [],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.exitCode).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("run calls event callbacks", async () => {
|
|
91
|
+
const agent = createMockAgent({
|
|
92
|
+
logs: [{ category: "info", message: "Test log" }],
|
|
93
|
+
output: ["Test output"],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const receivedLogs: string[] = [];
|
|
97
|
+
const receivedOutput: string[] = [];
|
|
98
|
+
|
|
99
|
+
await agent.run({
|
|
100
|
+
model: "test",
|
|
101
|
+
prompt: "test",
|
|
102
|
+
files: [],
|
|
103
|
+
events: {
|
|
104
|
+
onLog: (entry) => receivedLogs.push(entry.message),
|
|
105
|
+
onOutput: (out) => receivedOutput.push(out.text),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(receivedLogs).toEqual(["Test log"]);
|
|
110
|
+
expect(receivedOutput).toEqual(["Test output"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("configure updates agent behavior", async () => {
|
|
114
|
+
const agent = createMockAgent();
|
|
115
|
+
|
|
116
|
+
// Initial run
|
|
117
|
+
let result = await agent.run({
|
|
118
|
+
model: "test",
|
|
119
|
+
prompt: "test",
|
|
120
|
+
files: [],
|
|
121
|
+
});
|
|
122
|
+
expect(result.exitCode).toBe(0);
|
|
123
|
+
|
|
124
|
+
// Reconfigure
|
|
125
|
+
agent.configure({
|
|
126
|
+
exitCode: 2,
|
|
127
|
+
logs: [{ category: "error", message: "Reconfigured error" }],
|
|
128
|
+
output: ["Reconfigured output"],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Run again
|
|
132
|
+
result = await agent.run({
|
|
133
|
+
model: "test",
|
|
134
|
+
prompt: "test",
|
|
135
|
+
files: [],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result.exitCode).toBe(2);
|
|
139
|
+
expect(result.logs[0]!.message).toBe("Reconfigured error");
|
|
140
|
+
expect(result.output[0]!.text).toBe("Reconfigured output");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("run respects delay configuration", async () => {
|
|
144
|
+
const agent = createMockAgent({ delay: 50 });
|
|
145
|
+
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
await agent.run({
|
|
148
|
+
model: "test",
|
|
149
|
+
prompt: "test",
|
|
150
|
+
files: [],
|
|
151
|
+
});
|
|
152
|
+
const elapsed = Date.now() - start;
|
|
153
|
+
|
|
154
|
+
expect(elapsed).toBeGreaterThanOrEqual(49); // 50ms delay, but allow for some jitter
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("helper functions", () => {
|
|
159
|
+
test("createLogEntry creates entry with timestamp", () => {
|
|
160
|
+
const before = new Date();
|
|
161
|
+
const entry = createLogEntry("success", "Test message");
|
|
162
|
+
const after = new Date();
|
|
163
|
+
|
|
164
|
+
expect(entry.category).toBe("success");
|
|
165
|
+
expect(entry.message).toBe("Test message");
|
|
166
|
+
expect(entry.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
167
|
+
expect(entry.timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("createAgentOutput creates output with timestamp", () => {
|
|
171
|
+
const before = new Date();
|
|
172
|
+
const output = createAgentOutput("Test output");
|
|
173
|
+
const after = new Date();
|
|
174
|
+
|
|
175
|
+
expect(output.text).toBe("Test output");
|
|
176
|
+
expect(output.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
177
|
+
expect(output.timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
178
|
+
});
|
|
179
|
+
});
|