boxsafe 1.0.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/.directory +2 -0
- package/.env.example +3 -0
- package/AUDIT_LANG.md +45 -0
- package/BOXSAFE_VERSION_NOTES.md +14 -0
- package/README.md +4 -0
- package/TODO.md +130 -0
- package/adapters/index.ts +27 -0
- package/adapters/primary/cli-adapter.ts +56 -0
- package/adapters/secondary/filesystem/node-filesystem.ts +307 -0
- package/adapters/secondary/system/configuration.ts +147 -0
- package/ai/caller.ts +42 -0
- package/ai/label.ts +33 -0
- package/ai/modelConfig.ts +236 -0
- package/ai/provider.ts +111 -0
- package/boxsafe.config.json +68 -0
- package/core/auth/dasktop/cred/CRED.md +112 -0
- package/core/auth/dasktop/cred/credLinux.ts +82 -0
- package/core/auth/dasktop/cred/credWin.ts +2 -0
- package/core/config/defaults/boxsafeDefaults.ts +67 -0
- package/core/config/defaults/index.ts +1 -0
- package/core/config/loadConfig.ts +133 -0
- package/core/loop/about.md +13 -0
- package/core/loop/boxConfig.ts +20 -0
- package/core/loop/buildExecCommand.ts +76 -0
- package/core/loop/cmd/execode.ts +121 -0
- package/core/loop/cmd/test.js +3 -0
- package/core/loop/execLoop.ts +341 -0
- package/core/loop/git/VERSIONING.md +17 -0
- package/core/loop/git/commands.ts +11 -0
- package/core/loop/git/gitClient.ts +78 -0
- package/core/loop/git/index.ts +99 -0
- package/core/loop/git/runVersionControlRunner.ts +33 -0
- package/core/loop/initNavigator.ts +44 -0
- package/core/loop/initTasksManager.ts +35 -0
- package/core/loop/runValidation.ts +25 -0
- package/core/loop/tasks/AGENT-TASKS.md +36 -0
- package/core/loop/tasks/index.ts +96 -0
- package/core/loop/toolCalls.ts +168 -0
- package/core/loop/toolDispatcher.ts +146 -0
- package/core/loop/traceLogger.ts +106 -0
- package/core/loop/types.ts +26 -0
- package/core/loop/versionControlAdapter.ts +36 -0
- package/core/loop/waterfall.ts +404 -0
- package/core/loop/writeArtifactAtomically.ts +13 -0
- package/core/navigate/NAVIGATE.md +186 -0
- package/core/navigate/about.md +128 -0
- package/core/navigate/examples.ts +367 -0
- package/core/navigate/handler.ts +148 -0
- package/core/navigate/index.ts +32 -0
- package/core/navigate/navigate.test.ts +372 -0
- package/core/navigate/navigator.ts +437 -0
- package/core/navigate/types.ts +132 -0
- package/core/navigate/utils.ts +146 -0
- package/core/paths/paths.ts +33 -0
- package/core/ports/index.ts +271 -0
- package/core/segments/CONVENTIONS.md +30 -0
- package/core/segments/loop/index.ts +18 -0
- package/core/segments/map.ts +56 -0
- package/core/segments/navigate/index.ts +20 -0
- package/core/segments/versionControl/index.ts +18 -0
- package/core/util/logger.ts +128 -0
- package/docs/AGENT-TASKS.md +36 -0
- package/docs/ARQUITETURA_CORRECAO.md +121 -0
- package/docs/CONVENTIONS.md +30 -0
- package/docs/CRED.md +112 -0
- package/docs/L_RAG.md +567 -0
- package/docs/NAVIGATE.md +186 -0
- package/docs/PRIMARY_ACTORS.md +78 -0
- package/docs/SECONDARY_ACTORS.md +174 -0
- package/docs/VERSIONING.md +17 -0
- package/docs/boxsafe.config.md +472 -0
- package/eslint.config.mts +15 -0
- package/main.ts +53 -0
- package/memo/generated/codelog.md +13 -0
- package/memo/state/tasks/state.json +6 -0
- package/memo/state/tasks/tasks/task_001.md +2 -0
- package/memo/states-logs/logs.txt +7 -0
- package/memo/states-logs/trace-mljvrxvi-9g0k4q.jsonl +11 -0
- package/memo/states-logs/trace-mljvvc9j-pe9ekj.jsonl +11 -0
- package/memo/states-logs/trace-mljvvm1c-wbnqzp.jsonl +11 -0
- package/memo/states-logs/trace-mljxecwn-9xh3nw.jsonl +11 -0
- package/memo/states-logs/trace-mljxqkfm-ipijik.jsonl +11 -0
- package/memo/states-logs/trace-mljxwtrw-3fanky.jsonl +11 -0
- package/memo/states-logs/trace-mljxzen3-m8iinh.jsonl +11 -0
- package/memo/states-logs/trace-mljyucef-td6odn.jsonl +11 -0
- package/memo/states-logs/trace-mljyuprw-b1a6f4.jsonl +11 -0
- package/memo/states-logs/trace-mljyvefl-b6yoce.jsonl +11 -0
- package/memo/states-logs/trace-mljyxjo4-n7ibj2.jsonl +13 -0
- package/memo/states-logs/trace-mljziez5-8drqtn.jsonl +13 -0
- package/memo/states-logs/trace-mljziulp-dtd03z.jsonl +13 -0
- package/memo/states-logs/trace-mljzjwrq-1p2krb.jsonl +13 -0
- package/memo/states-logs/trace-mljzl0i7-b1cqa6.jsonl +13 -0
- package/memo/states-logs/trace-mljzmlk6-7kdyls.jsonl +13 -0
- package/memo/states-logs/trace-mlk0oj25-xa3dcu.jsonl +13 -0
- package/memo/states-logs/trace-mlk1x59q-713huj.jsonl +14 -0
- package/memo/states-logs/trace-mlk22dz8-7fd6hq.jsonl +14 -0
- package/memo/states-logs/trace-mlk241uy-wmx907.jsonl +14 -0
- package/memo/states-logs/trace-mlk2bf5r-yoh1vg.jsonl +15 -0
- package/package.json +44 -0
- package/pnpm-workspace.yaml +4 -0
- package/prompt_improvement_example.md +55 -0
- package/remove.txt +1 -0
- package/tests/adapters.test.ts +128 -0
- package/tests/extractCode.test.ts +26 -0
- package/tests/integration.test.ts +83 -0
- package/tests/loadConfig.test.ts +25 -0
- package/tests/navigatorBoundary.test.ts +17 -0
- package/tests/ports.test.ts +84 -0
- package/tests/runAllTests.ts +49 -0
- package/tests/toolCalls.test.ts +149 -0
- package/tests/waterfall.test.ts +52 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +17 -0
- package/types.d.ts +96 -0
- package/util/ANSI.ts +29 -0
- package/util/extractCode.ts +217 -0
- package/util/extractToolCalls.ts +80 -0
- package/util/logger.ts +125 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { waterfall } from '@core/loop/waterfall';
|
|
2
|
+
import { Logger } from '@core/util/logger';
|
|
3
|
+
|
|
4
|
+
const logger = Logger.createModuleLogger('RunValidation');
|
|
5
|
+
|
|
6
|
+
type RunValidationArgs = {
|
|
7
|
+
execResult: any;
|
|
8
|
+
pathOutput: string;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function runValidation({ execResult, pathOutput, signal }: RunValidationArgs): Promise<any> {
|
|
13
|
+
if (signal?.aborted) throw new Error('Aborted');
|
|
14
|
+
try {
|
|
15
|
+
return await waterfall({
|
|
16
|
+
exec: execResult,
|
|
17
|
+
artifacts: {
|
|
18
|
+
outputFile: pathOutput,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
} catch (err: any) {
|
|
22
|
+
logger.error(`Waterfall error: ${err?.message ?? err}`);
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
**Tasks module for BoxSafe — core/loop/tasks**
|
|
2
|
+
|
|
3
|
+
Overview
|
|
4
|
+
- The `TasksManager` provides a minimal, predictable mechanism to load a TODO file,
|
|
5
|
+
split it into small tasks, persist task artifacts and a lightweight runtime state
|
|
6
|
+
under `memo/state/tasks`.
|
|
7
|
+
|
|
8
|
+
Design goals
|
|
9
|
+
- Keep implementation tiny and synchronous-friendly (async IO only).
|
|
10
|
+
- Avoid complex formats: accept either `- item` lines or paragraphs separated by
|
|
11
|
+
blank lines in the TODO file.
|
|
12
|
+
- Persist each task as `memo/state/tasks/tasks/task_###.md` and keep state in
|
|
13
|
+
`memo/state/tasks/state.json` with `current` and `done[]` fields.
|
|
14
|
+
|
|
15
|
+
How it is used
|
|
16
|
+
- The `execLoop` integrates `TasksManager` when `boxsafe.config.json` contains
|
|
17
|
+
`project.todo` pointing to a TODO file. The loop will use the current task as
|
|
18
|
+
the agent prompt, and when the agent completes a task it marks it done and
|
|
19
|
+
moves to the next. Only when all tasks complete does the loop perform the
|
|
20
|
+
normal after-success flow (versioning, notes, etc.).
|
|
21
|
+
|
|
22
|
+
Files
|
|
23
|
+
- `index.ts` — exports `TasksManager` class.
|
|
24
|
+
|
|
25
|
+
Behavior summary
|
|
26
|
+
- `TasksManager.init()` creates `memo/state/tasks` and writes individual task
|
|
27
|
+
files and `state.json` when needed.
|
|
28
|
+
- `getCurrentTask()` returns the current task prompt or `null` when finished.
|
|
29
|
+
- `markCurrentDone()` marks current task as done and advances to the next.
|
|
30
|
+
- `isFinished()` signals completion of all tasks.
|
|
31
|
+
|
|
32
|
+
Notes
|
|
33
|
+
- The module intentionally keeps parsing and behavior simple to be robust and
|
|
34
|
+
easy to audit. If you want more advanced scheduling, priorities or parallel
|
|
35
|
+
execution, we can extend it, but this initial version focuses on correctness
|
|
36
|
+
and minimal surface area.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { TASKS_STATE_DIR, TASKS_TASKS_DIR, TASKS_STATE_FILE } from '@core/paths/paths';
|
|
5
|
+
|
|
6
|
+
export class TasksManager {
|
|
7
|
+
todoPath: string;
|
|
8
|
+
baseDir: string;
|
|
9
|
+
tasksDir: string;
|
|
10
|
+
statePath: string;
|
|
11
|
+
tasks: string[] = [];
|
|
12
|
+
state: { current: number; done: boolean[] } = { current: 0, done: [] };
|
|
13
|
+
|
|
14
|
+
constructor(todoPath: string, baseDir?: string) {
|
|
15
|
+
this.todoPath = path.resolve(todoPath);
|
|
16
|
+
this.baseDir = baseDir ? path.resolve(baseDir) : TASKS_STATE_DIR;
|
|
17
|
+
this.tasksDir = baseDir ? path.join(this.baseDir, 'tasks') : TASKS_TASKS_DIR;
|
|
18
|
+
this.statePath = baseDir ? path.join(this.baseDir, 'state.json') : TASKS_STATE_FILE;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async init() {
|
|
22
|
+
// ensure directories
|
|
23
|
+
await fs.mkdir(this.tasksDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// load or create tasks
|
|
26
|
+
if (fsSync.existsSync(this.statePath)) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readFile(this.statePath, 'utf-8');
|
|
29
|
+
this.state = JSON.parse(raw);
|
|
30
|
+
// load tasks files if present
|
|
31
|
+
const files = await fs.readdir(this.tasksDir);
|
|
32
|
+
this.tasks = [];
|
|
33
|
+
for (const f of files.sort()) {
|
|
34
|
+
const content = await fs.readFile(path.join(this.tasksDir, f), 'utf-8');
|
|
35
|
+
this.tasks.push(content);
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// fallback to re-scan todo
|
|
39
|
+
await this._prepareFromTodo();
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
await this._prepareFromTodo();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async _prepareFromTodo() {
|
|
47
|
+
this.tasks = [];
|
|
48
|
+
this.state = { current: 0, done: [] };
|
|
49
|
+
if (!fsSync.existsSync(this.todoPath)) return;
|
|
50
|
+
const raw = await fs.readFile(this.todoPath, 'utf-8');
|
|
51
|
+
// parse simple list format: lines starting with '-' or split by double-newline
|
|
52
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
53
|
+
let items: string[] = [];
|
|
54
|
+
const dashItems = lines.filter((l) => l.startsWith('- ')).map((l) => l.replace(/^-\s+/, ''));
|
|
55
|
+
if (dashItems.length > 0) items = dashItems;
|
|
56
|
+
else items = raw.split(/\n\s*\n/).map(s=>s.trim()).filter(Boolean);
|
|
57
|
+
|
|
58
|
+
// write tasks files
|
|
59
|
+
for (let i = 0; i < items.length; i++) {
|
|
60
|
+
const filename = `task_${String(i+1).padStart(3,'0')}.md`;
|
|
61
|
+
const item = items[i] ?? '';
|
|
62
|
+
await fs.writeFile(path.join(this.tasksDir, filename), item, 'utf-8');
|
|
63
|
+
this.tasks.push(item);
|
|
64
|
+
this.state.done.push(false);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await this._saveState();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async _saveState() {
|
|
71
|
+
await fs.writeFile(this.statePath, JSON.stringify(this.state, null, 2), 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getCurrentTask(): string | null {
|
|
75
|
+
if (this.state.current >= this.tasks.length) return null;
|
|
76
|
+
return this.tasks[this.state.current] ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async markCurrentDone() {
|
|
80
|
+
if (this.state.current >= this.tasks.length) return;
|
|
81
|
+
this.state.done[this.state.current] = true;
|
|
82
|
+
// advance to next not-done
|
|
83
|
+
let idx = this.state.current + 1;
|
|
84
|
+
while (idx < this.tasks.length && this.state.done[idx]) idx++;
|
|
85
|
+
this.state.current = idx;
|
|
86
|
+
await this._saveState();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
isFinished(): boolean {
|
|
90
|
+
return this.state.current >= this.tasks.length;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
total(): number { return this.tasks.length; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default TasksManager;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Logger } from '@core/util/logger';
|
|
2
|
+
|
|
3
|
+
const logger = Logger.createModuleLogger('ToolCalls');
|
|
4
|
+
|
|
5
|
+
export type ToolName = 'navigate' | 'versionControl';
|
|
6
|
+
|
|
7
|
+
export type NavigateOp = 'list' | 'read' | 'write' | 'mkdir' | 'delete' | 'stat';
|
|
8
|
+
|
|
9
|
+
export type NavigateToolCall = {
|
|
10
|
+
tool: 'navigate';
|
|
11
|
+
params: {
|
|
12
|
+
op: NavigateOp;
|
|
13
|
+
path?: string;
|
|
14
|
+
content?: string;
|
|
15
|
+
writeOptions?: { append?: boolean; createDirs?: boolean };
|
|
16
|
+
mkdirOptions?: { recursive?: boolean };
|
|
17
|
+
deleteOptions?: { recursive?: boolean };
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type VersionControlToolCall = {
|
|
22
|
+
tool: 'versionControl';
|
|
23
|
+
params: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ToolCall = NavigateToolCall | VersionControlToolCall;
|
|
27
|
+
|
|
28
|
+
export type ToolCallParseError = {
|
|
29
|
+
ok: false;
|
|
30
|
+
error: string;
|
|
31
|
+
fence?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ToolCallParseResult = {
|
|
35
|
+
ok: true;
|
|
36
|
+
calls: ToolCall[];
|
|
37
|
+
errors: ToolCallParseError[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
41
|
+
return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isParseError(v: ToolCall | ToolCallParseError): v is ToolCallParseError {
|
|
45
|
+
return isPlainObject(v) && (v as any).ok === false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isNavigateOp(v: unknown): v is NavigateOp {
|
|
49
|
+
return (
|
|
50
|
+
v === 'list' ||
|
|
51
|
+
v === 'read' ||
|
|
52
|
+
v === 'write' ||
|
|
53
|
+
v === 'mkdir' ||
|
|
54
|
+
v === 'delete' ||
|
|
55
|
+
v === 'stat'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseOneToolCall(obj: unknown): ToolCall | ToolCallParseError {
|
|
60
|
+
if (!isPlainObject(obj)) return { ok: false, error: 'tool call must be an object' };
|
|
61
|
+
|
|
62
|
+
const tool = obj.tool;
|
|
63
|
+
if (tool !== 'navigate' && tool !== 'versionControl') {
|
|
64
|
+
return { ok: false, error: `unknown tool: ${String(tool)}` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const paramsRaw = obj.params;
|
|
68
|
+
const params = isPlainObject(paramsRaw) ? paramsRaw : {};
|
|
69
|
+
|
|
70
|
+
if (tool === 'navigate') {
|
|
71
|
+
const op = params.op;
|
|
72
|
+
if (!isNavigateOp(op)) {
|
|
73
|
+
return { ok: false, error: `navigate.op must be one of list|read|write|mkdir|delete|stat (got: ${String(op)})` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const path = typeof params.path === 'string' ? params.path : undefined;
|
|
77
|
+
const content = typeof params.content === 'string' ? params.content : undefined;
|
|
78
|
+
|
|
79
|
+
if ((op === 'read' || op === 'write' || op === 'mkdir' || op === 'delete' || op === 'stat') && !path) {
|
|
80
|
+
return { ok: false, error: `navigate.op=${op} requires params.path` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (op === 'write' && content === undefined) {
|
|
84
|
+
return { ok: false, error: `navigate.op=write requires params.content` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const writeOptions = isPlainObject(params.writeOptions) ? params.writeOptions : undefined;
|
|
88
|
+
const mkdirOptions = isPlainObject(params.mkdirOptions) ? params.mkdirOptions : undefined;
|
|
89
|
+
const deleteOptions = isPlainObject(params.deleteOptions) ? params.deleteOptions : undefined;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
tool: 'navigate',
|
|
93
|
+
params: {
|
|
94
|
+
op,
|
|
95
|
+
...(path ? { path } : {}),
|
|
96
|
+
...(content !== undefined ? { content } : {}),
|
|
97
|
+
...(writeOptions
|
|
98
|
+
? {
|
|
99
|
+
writeOptions: {
|
|
100
|
+
...(typeof writeOptions.append === 'boolean' ? { append: writeOptions.append } : {}),
|
|
101
|
+
...(typeof writeOptions.createDirs === 'boolean' ? { createDirs: writeOptions.createDirs } : {}),
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
: {}),
|
|
105
|
+
...(mkdirOptions
|
|
106
|
+
? {
|
|
107
|
+
mkdirOptions: {
|
|
108
|
+
...(typeof mkdirOptions.recursive === 'boolean' ? { recursive: mkdirOptions.recursive } : {}),
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
...(deleteOptions
|
|
113
|
+
? {
|
|
114
|
+
deleteOptions: {
|
|
115
|
+
...(typeof deleteOptions.recursive === 'boolean' ? { recursive: deleteOptions.recursive } : {}),
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
tool: 'versionControl',
|
|
125
|
+
params,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function parseToolCallsFromMarkdown(markdown: string): ToolCallParseResult {
|
|
130
|
+
const calls: ToolCall[] = [];
|
|
131
|
+
const errors: ToolCallParseError[] = [];
|
|
132
|
+
|
|
133
|
+
logger.debug(`Parsing markdown for tool calls (${markdown.length} chars)`);
|
|
134
|
+
|
|
135
|
+
// FIXED: Changed regex to specifically target json-tool blocks
|
|
136
|
+
// The old regex /```(?:json|json-tool)\s*([\s\S]*?)\s*```/g was capturing the wrong group
|
|
137
|
+
const jsonFenceRe = /```json-tool\s*([\s\S]*?)\s*```/g;
|
|
138
|
+
let m: RegExpExecArray | null;
|
|
139
|
+
|
|
140
|
+
while ((m = jsonFenceRe.exec(markdown)) !== null) {
|
|
141
|
+
const fence = (m[1] ?? '').trim();
|
|
142
|
+
logger.debug(`Found potential tool call: ${fence.substring(0, 100)}...`);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const obj = JSON.parse(fence);
|
|
146
|
+
logger.debug(`JSON parsed successfully: ${JSON.stringify(obj)}`);
|
|
147
|
+
|
|
148
|
+
const parsed = parseOneToolCall(obj);
|
|
149
|
+
|
|
150
|
+
if (isParseError(parsed)) {
|
|
151
|
+
logger.warn(`Invalid tool call: ${parsed.error}`);
|
|
152
|
+
errors.push({ ...parsed, fence });
|
|
153
|
+
} else {
|
|
154
|
+
logger.info(`Valid tool call detected: ${parsed.tool}`);
|
|
155
|
+
calls.push(parsed);
|
|
156
|
+
}
|
|
157
|
+
} catch (e: any) {
|
|
158
|
+
logger.error(`JSON parse failed: ${e?.message ?? String(e)}`);
|
|
159
|
+
errors.push({ ok: false, error: `invalid JSON: ${e?.message ?? String(e)}`, fence });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
logger.info(`Tool calls parsed: ${calls.length} valid, ${errors.length} errors`);
|
|
164
|
+
return { ok: true, calls, errors };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Export helper functions for reuse
|
|
168
|
+
export { parseOneToolCall, isParseError };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Navigator } from '@core/navigate';
|
|
2
|
+
import { parseToolCallsFromMarkdown } from './toolCalls';
|
|
3
|
+
import type { ToolCall } from './toolCalls';
|
|
4
|
+
import type { TraceCtx } from './traceLogger';
|
|
5
|
+
import { Logger } from '@core/util/logger';
|
|
6
|
+
|
|
7
|
+
const logger = Logger.createModuleLogger('ToolDispatcher');
|
|
8
|
+
|
|
9
|
+
type AnsiLike = { Cyan: string; Yellow: string; Red: string; Reset: string };
|
|
10
|
+
|
|
11
|
+
type AttemptVersionControl = (opts: any) => Promise<any>;
|
|
12
|
+
|
|
13
|
+
type TraceEmit = (event: string, ctx?: TraceCtx, data?: Record<string, unknown>) => Promise<void>;
|
|
14
|
+
|
|
15
|
+
type DispatchArgs = {
|
|
16
|
+
markdown: string;
|
|
17
|
+
navigator: Navigator | null;
|
|
18
|
+
boxConfig: any;
|
|
19
|
+
ANSI: AnsiLike;
|
|
20
|
+
attemptVersionControl: AttemptVersionControl;
|
|
21
|
+
vcAutoPushConfig: boolean;
|
|
22
|
+
traceEmit?: TraceEmit;
|
|
23
|
+
traceCtx?: TraceCtx;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export async function dispatchToolCalls({
|
|
27
|
+
markdown,
|
|
28
|
+
navigator,
|
|
29
|
+
boxConfig,
|
|
30
|
+
ANSI,
|
|
31
|
+
attemptVersionControl,
|
|
32
|
+
vcAutoPushConfig,
|
|
33
|
+
traceEmit,
|
|
34
|
+
traceCtx,
|
|
35
|
+
}: DispatchArgs): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
logger.debug(`Dispatching tool calls from markdown (${markdown.length} chars)`);
|
|
38
|
+
logger.debug(`Navigator available: ${!!navigator}`);
|
|
39
|
+
|
|
40
|
+
const parsed = parseToolCallsFromMarkdown(markdown);
|
|
41
|
+
await traceEmit?.('toolcalls.parsed', traceCtx, {
|
|
42
|
+
calls: parsed.calls.length,
|
|
43
|
+
errors: parsed.errors.length,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
logger.info(`Tool calls parsed: ${parsed.calls.length} valid, ${parsed.errors.length} errors`);
|
|
47
|
+
|
|
48
|
+
for (const e of parsed.errors) {
|
|
49
|
+
logger.warn(`Invalid tool call: ${e.error}`);
|
|
50
|
+
await traceEmit?.('toolcall.invalid', traceCtx, { error: e.error });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (parsed.calls.length === 0) {
|
|
54
|
+
logger.debug('No valid tool calls to execute');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logger.info(`Executing ${parsed.calls.length} tool calls...`);
|
|
59
|
+
for (const call of parsed.calls) {
|
|
60
|
+
await executeOneToolCall(call);
|
|
61
|
+
}
|
|
62
|
+
} catch (toolErr: any) {
|
|
63
|
+
logger.error(`Tool parsing failed: ${toolErr?.message ?? toolErr}`);
|
|
64
|
+
await traceEmit?.('toolcalls.failed', traceCtx, { error: toolErr?.message ?? String(toolErr) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function executeOneToolCall(call: ToolCall): Promise<void> {
|
|
68
|
+
logger.debug(`Executing tool call: ${JSON.stringify(call)}`);
|
|
69
|
+
logger.info(`detected tool call: ${call.tool}`);
|
|
70
|
+
await traceEmit?.('toolcall.detected', traceCtx, { tool: call.tool });
|
|
71
|
+
|
|
72
|
+
if (call.tool === 'navigate') {
|
|
73
|
+
logger.debug(`Processing navigate tool call with op: ${call.params.op}`);
|
|
74
|
+
|
|
75
|
+
if (!navigator) {
|
|
76
|
+
logger.warn(`navigate requested but navigator is not initialized`);
|
|
77
|
+
await traceEmit?.('toolcall.navigate.skipped', traceCtx, { reason: 'navigator_not_initialized' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { op } = call.params;
|
|
82
|
+
let toolRes: any = null;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
if (op === 'write') {
|
|
86
|
+
logger.debug(`Writing file: ${call.params.path}`);
|
|
87
|
+
toolRes = await navigator.writeFile(call.params.path!, call.params.content!, call.params.writeOptions);
|
|
88
|
+
} else if (op === 'mkdir') {
|
|
89
|
+
logger.debug(`Creating directory: ${call.params.path}`);
|
|
90
|
+
toolRes = await navigator.createDirectory(call.params.path!, call.params.mkdirOptions);
|
|
91
|
+
} else if (op === 'delete') {
|
|
92
|
+
logger.debug(`Deleting: ${call.params.path}`);
|
|
93
|
+
toolRes = await navigator.delete(call.params.path!, call.params.deleteOptions);
|
|
94
|
+
} else if (op === 'read') {
|
|
95
|
+
logger.debug(`Reading file: ${call.params.path}`);
|
|
96
|
+
toolRes = await navigator.readFile(call.params.path!);
|
|
97
|
+
} else if (op === 'list') {
|
|
98
|
+
logger.debug(`Listing directory: ${call.params.path ?? '.'}`);
|
|
99
|
+
toolRes = await navigator.listDirectory(call.params.path ?? '.');
|
|
100
|
+
} else if (op === 'stat') {
|
|
101
|
+
logger.debug(`Getting metadata: ${call.params.path}`);
|
|
102
|
+
toolRes = await navigator.getMetadata(call.params.path!);
|
|
103
|
+
} else {
|
|
104
|
+
logger.warn(`unknown navigate op: ${String(op)}`);
|
|
105
|
+
await traceEmit?.('toolcall.navigate.unknown_op', traceCtx, { op: String(op) });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
logger.info(`Navigate operation ${op} completed successfully`);
|
|
109
|
+
} catch (innerErr: any) {
|
|
110
|
+
logger.error(`navigate execution error: ${innerErr?.message ?? innerErr}`);
|
|
111
|
+
await traceEmit?.('toolcall.navigate.error', traceCtx, { error: innerErr?.message ?? String(innerErr) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
logger.debug(`Navigate result: ${JSON.stringify(toolRes ?? { ok: false })}`);
|
|
115
|
+
logger.info(`navigate result: ${JSON.stringify(toolRes ?? { ok: false })}`);
|
|
116
|
+
await traceEmit?.('toolcall.navigate.result', traceCtx, {
|
|
117
|
+
op,
|
|
118
|
+
ok: Boolean(toolRes?.ok),
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (call.tool === 'versionControl') {
|
|
124
|
+
const allowed = Boolean(boxConfig.project?.versionControl?.before) || Boolean(boxConfig.project?.versionControl?.after);
|
|
125
|
+
if (!allowed) {
|
|
126
|
+
logger.warn(`versionControl requested but not authorized in config`);
|
|
127
|
+
await traceEmit?.('toolcall.versionControl.skipped', traceCtx, { reason: 'not_authorized' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const params: any = { ...(call.params ?? {}) };
|
|
133
|
+
if (params.autoPush === undefined) params.autoPush = vcAutoPushConfig;
|
|
134
|
+
const vcRes = await attemptVersionControl(params);
|
|
135
|
+
logger.info(`versionControl result: ${JSON.stringify(vcRes ?? { ok: true })}`);
|
|
136
|
+
await traceEmit?.('toolcall.versionControl.result', traceCtx, { ok: true });
|
|
137
|
+
} catch (vcErr: any) {
|
|
138
|
+
logger.error(`versionControl error: ${vcErr?.message ?? vcErr}`);
|
|
139
|
+
await traceEmit?.('toolcall.versionControl.error', traceCtx, { error: vcErr?.message ?? String(vcErr) });
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
logger.warn(`unhandled tool: ${String((call as any).tool)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { mkdir, appendFile, readdir, stat, unlink } from 'node:fs/promises';
|
|
3
|
+
import { STATES_LOGS_DIR } from '@core/paths/paths';
|
|
4
|
+
import { Logger } from '@core/util/logger';
|
|
5
|
+
|
|
6
|
+
export type TraceCtx = {
|
|
7
|
+
runId: string;
|
|
8
|
+
iter?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type TraceEvent = {
|
|
12
|
+
ts: string;
|
|
13
|
+
runId: string;
|
|
14
|
+
iter?: number;
|
|
15
|
+
event: string;
|
|
16
|
+
data?: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TraceOptions = {
|
|
20
|
+
runId: string;
|
|
21
|
+
retain?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function safeJsonStringify(v: unknown): string {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(v);
|
|
27
|
+
} catch {
|
|
28
|
+
return JSON.stringify({ error: 'unstringifiable' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeRunId(): string {
|
|
33
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
34
|
+
return `${Date.now().toString(36)}-${rand}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function applyRetention(retain: number): Promise<void> {
|
|
38
|
+
const entries = await readdir(STATES_LOGS_DIR, { withFileTypes: true }).catch(() => []);
|
|
39
|
+
const files = entries
|
|
40
|
+
.filter((e) => e.isFile() && e.name.startsWith('trace-') && e.name.endsWith('.jsonl'))
|
|
41
|
+
.map((e) => e.name);
|
|
42
|
+
|
|
43
|
+
const withTimes = await Promise.all(
|
|
44
|
+
files.map(async (name) => {
|
|
45
|
+
const p = path.join(STATES_LOGS_DIR, name);
|
|
46
|
+
const s = await stat(p).catch(() => null);
|
|
47
|
+
return { name, mtimeMs: s?.mtimeMs ?? 0 };
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
withTimes.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
52
|
+
const toDelete = withTimes.slice(retain);
|
|
53
|
+
await Promise.all(toDelete.map((f) => unlink(path.join(STATES_LOGS_DIR, f.name)).catch(() => null)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createTraceLogger(opts?: Partial<TraceOptions>) {
|
|
57
|
+
const runId = opts?.runId ?? process.env.BOXSAFE_RUN_ID ?? makeRunId();
|
|
58
|
+
const retain = typeof opts?.retain === 'number' ? opts.retain : Number(process.env.BOXSAFE_TRACE_RETAIN ?? 20);
|
|
59
|
+
const filePath = path.join(STATES_LOGS_DIR, `trace-${runId}.jsonl`);
|
|
60
|
+
|
|
61
|
+
let initialized = false;
|
|
62
|
+
|
|
63
|
+
async function ensureInit() {
|
|
64
|
+
if (initialized) return;
|
|
65
|
+
await mkdir(STATES_LOGS_DIR, { recursive: true });
|
|
66
|
+
if (Number.isFinite(retain) && retain > 0) {
|
|
67
|
+
await applyRetention(retain);
|
|
68
|
+
}
|
|
69
|
+
initialized = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function emit(event: string, ctx?: TraceCtx, data?: Record<string, unknown>) {
|
|
73
|
+
await ensureInit();
|
|
74
|
+
const entry: TraceEvent = {
|
|
75
|
+
ts: new Date().toISOString(),
|
|
76
|
+
runId,
|
|
77
|
+
...(typeof ctx?.iter === 'number' ? { iter: ctx.iter } : {}),
|
|
78
|
+
event,
|
|
79
|
+
...(data ? { data } : {}),
|
|
80
|
+
};
|
|
81
|
+
await appendFile(filePath, `${safeJsonStringify(entry)}\n`, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function prefix(ctx?: TraceCtx): string {
|
|
85
|
+
const parts = [`run=${runId}`];
|
|
86
|
+
if (typeof ctx?.iter === 'number') parts.push(`iter=${ctx.iter}`);
|
|
87
|
+
return `[${parts.join('][')}]`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function wrapLogger(ctx?: TraceCtx) {
|
|
91
|
+
const logger = Logger.createModuleLogger('Trace');
|
|
92
|
+
const p = prefix(ctx);
|
|
93
|
+
return {
|
|
94
|
+
info: (...a: any[]) => logger.info(`${p} ${a.join(' ')}`),
|
|
95
|
+
warn: (...a: any[]) => logger.warn(`${p} ${a.join(' ')}`),
|
|
96
|
+
error: (...a: any[]) => logger.error(`${p} ${a.join(' ')}`),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
runId,
|
|
102
|
+
filePath,
|
|
103
|
+
emit,
|
|
104
|
+
wrapLogger,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LModel, LService } from '@ai/label';
|
|
2
|
+
import type { CommandRun } from "../../types";
|
|
3
|
+
import type { Navigator } from '@core/navigate';
|
|
4
|
+
|
|
5
|
+
export interface LoopOptions {
|
|
6
|
+
service: LService;
|
|
7
|
+
model: LModel;
|
|
8
|
+
initialPrompt: string;
|
|
9
|
+
cmd: CommandRun;
|
|
10
|
+
lang: string;
|
|
11
|
+
pathOutput: string;
|
|
12
|
+
maxIterations?: number;
|
|
13
|
+
limit?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
pathGeneratedMarkdown?: string;
|
|
16
|
+
navigator?: Navigator;
|
|
17
|
+
workspace?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoopResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
iterations: number;
|
|
23
|
+
verdict?: unknown;
|
|
24
|
+
artifacts?: { outputFile?: string };
|
|
25
|
+
navigator?: Navigator | null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { runVersionControl } from '@core/loop/git';
|
|
2
|
+
import { Logger } from '@core/util/logger';
|
|
3
|
+
|
|
4
|
+
const logger = Logger.createModuleLogger('VersionControlAdapter');
|
|
5
|
+
|
|
6
|
+
type AttemptOptions = {
|
|
7
|
+
repoPath: string;
|
|
8
|
+
commitMessage: string;
|
|
9
|
+
autoPush: boolean;
|
|
10
|
+
generateNotes: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createVersionControlAttemptRunner() {
|
|
14
|
+
return async function attemptVersionControl(opts: AttemptOptions) {
|
|
15
|
+
const maxAttempts = 3;
|
|
16
|
+
let attempt = 0;
|
|
17
|
+
let lastErr: any = null;
|
|
18
|
+
logger.info(`attempting runVersionControl with opts=${JSON.stringify(opts)}`);
|
|
19
|
+
while (attempt < maxAttempts) {
|
|
20
|
+
attempt++;
|
|
21
|
+
try {
|
|
22
|
+
logger.debug(`attempt ${attempt}`);
|
|
23
|
+
const res = await runVersionControl(opts);
|
|
24
|
+
logger.info(`result (attempt ${attempt}): ${JSON.stringify(res)}`);
|
|
25
|
+
return res;
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
lastErr = err;
|
|
28
|
+
logger.warn(`attempt ${attempt} failed: ${err?.message ?? err}`);
|
|
29
|
+
const backoff = 300 * Math.pow(2, attempt - 1);
|
|
30
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
logger.error(`all ${maxAttempts} attempts failed: ${lastErr?.message ?? lastErr}`);
|
|
34
|
+
throw lastErr;
|
|
35
|
+
};
|
|
36
|
+
}
|