vgxness 0.1.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/LICENSE +9 -0
- package/README.md +110 -0
- package/dist/agents/agent-activation-service.js +144 -0
- package/dist/agents/agent-registry-service.js +46 -0
- package/dist/agents/agent-resolver.js +249 -0
- package/dist/agents/agent-seed-service.js +146 -0
- package/dist/agents/manager-profile-overlay-service.js +34 -0
- package/dist/agents/profile-model-routing.js +26 -0
- package/dist/agents/renderers/claude-renderer.js +98 -0
- package/dist/agents/renderers/index.js +16 -0
- package/dist/agents/renderers/json-renderer.js +87 -0
- package/dist/agents/renderers/opencode-renderer.js +100 -0
- package/dist/agents/renderers/provider-adapter.js +6 -0
- package/dist/agents/repositories/agents.js +185 -0
- package/dist/agents/repositories/manager-profile-overlays.js +81 -0
- package/dist/agents/schema.js +1 -0
- package/dist/cli/dashboard-operational-read-models.js +153 -0
- package/dist/cli/dashboard-renderer.js +109 -0
- package/dist/cli/dashboard-screen-renderers.js +332 -0
- package/dist/cli/dashboard-tui-read-model.js +71 -0
- package/dist/cli/dashboard-tui-state.js +218 -0
- package/dist/cli/dispatcher.js +2880 -0
- package/dist/cli/index.js +27 -0
- package/dist/cli/interactive-dashboard.js +29 -0
- package/dist/cli/mcp-start-path.js +21 -0
- package/dist/cli/setup-status-renderer.js +29 -0
- package/dist/cli/setup-wizard-read-model.js +56 -0
- package/dist/cli/setup-wizard-renderer.js +148 -0
- package/dist/cli/setup-wizard-state.js +82 -0
- package/dist/cli/tui-render-helpers.js +192 -0
- package/dist/export/redaction.js +71 -0
- package/dist/harness/tools/agents.js +245 -0
- package/dist/harness/tools/memory.js +29 -0
- package/dist/mcp/client-install-opencode-contract.js +227 -0
- package/dist/mcp/client-install-opencode.js +194 -0
- package/dist/mcp/client-setup-preview.js +38 -0
- package/dist/mcp/control-plane.js +175 -0
- package/dist/mcp/doctor.js +193 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/opencode-default-agent-config.js +156 -0
- package/dist/mcp/opencode-visibility.js +102 -0
- package/dist/mcp/schema.js +234 -0
- package/dist/mcp/stdio-server.js +56 -0
- package/dist/mcp/validation.js +761 -0
- package/dist/memory/import/dry-run-planner.js +58 -0
- package/dist/memory/import/index.js +3 -0
- package/dist/memory/import/observation-writer.js +220 -0
- package/dist/memory/import/package.js +178 -0
- package/dist/memory/memory-service.js +126 -0
- package/dist/memory/repositories/artifacts.js +41 -0
- package/dist/memory/repositories/observations.js +133 -0
- package/dist/memory/repositories/sessions.js +105 -0
- package/dist/memory/repositories/traces.js +58 -0
- package/dist/memory/schema.js +1 -0
- package/dist/memory/search.js +11 -0
- package/dist/memory/sqlite/database.js +97 -0
- package/dist/memory/sqlite/migrations/001_initial.sql +128 -0
- package/dist/memory/sqlite/migrations/002_observation_revisions.sql +14 -0
- package/dist/memory/sqlite/migrations/003_agent_registry.sql +26 -0
- package/dist/memory/sqlite/migrations/004_run_runtime.sql +62 -0
- package/dist/memory/sqlite/migrations/005_run_approvals.sql +20 -0
- package/dist/memory/sqlite/migrations/006_run_operation_attempts.sql +32 -0
- package/dist/memory/sqlite/migrations/007_abandoned_operation_attempts.sql +46 -0
- package/dist/memory/sqlite/migrations/008_run_execution_plan_events.sql +105 -0
- package/dist/memory/sqlite/migrations/009_multiple_operation_attempts.sql +73 -0
- package/dist/memory/sqlite/migrations/010_skill_registry.sql +66 -0
- package/dist/memory/sqlite/migrations/011_skill_usage_resolution_outcomes.sql +21 -0
- package/dist/memory/sqlite/migrations/012_skill_improvement_proposals.sql +37 -0
- package/dist/memory/sqlite/migrations/013_skill_evaluation_scenarios.sql +43 -0
- package/dist/memory/sqlite/migrations/014_manager_profile_overlays.sql +14 -0
- package/dist/memory/storage-paths.js +72 -0
- package/dist/orchestrator/natural-language-planner.js +191 -0
- package/dist/orchestrator/schema.js +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/policy-evaluator.js +109 -0
- package/dist/permissions/schema.js +1 -0
- package/dist/providers/opencode/injection-preview.js +134 -0
- package/dist/providers/opencode/manager-payload.js +129 -0
- package/dist/runs/execution-planning.js +117 -0
- package/dist/runs/operation-execution.js +1 -0
- package/dist/runs/operation-retry.js +124 -0
- package/dist/runs/repositories/runs.js +611 -0
- package/dist/runs/run-insights.js +145 -0
- package/dist/runs/run-service.js +713 -0
- package/dist/runs/run-snapshot-export-service.js +31 -0
- package/dist/runs/sandbox-process-execution.js +218 -0
- package/dist/runs/sandbox-worktree-planning.js +59 -0
- package/dist/runs/schema.js +1 -0
- package/dist/sdd/artifact-portability-service.js +118 -0
- package/dist/sdd/schema.js +17 -0
- package/dist/sdd/sdd-workflow-service.js +217 -0
- package/dist/setup/backup-rollback-service.js +76 -0
- package/dist/setup/index.js +3 -0
- package/dist/setup/providers/antigravity-setup-adapter.js +18 -0
- package/dist/setup/providers/claude-setup-adapter.js +30 -0
- package/dist/setup/providers/custom-setup-adapter.js +18 -0
- package/dist/setup/providers/index.js +6 -0
- package/dist/setup/providers/opencode-setup-adapter.js +104 -0
- package/dist/setup/providers/provider-setup-adapter.js +15 -0
- package/dist/setup/providers/provider-setup-registry.js +11 -0
- package/dist/setup/schema.js +1 -0
- package/dist/setup/setup-defaults.js +11 -0
- package/dist/setup/setup-lifecycle-service.js +175 -0
- package/dist/setup/setup-plan.js +105 -0
- package/dist/skills/repositories/skill-evaluation-scenarios.js +289 -0
- package/dist/skills/repositories/skill-improvement-proposals.js +288 -0
- package/dist/skills/repositories/skills.js +430 -0
- package/dist/skills/schema.js +1 -0
- package/dist/skills/skill-payload.js +94 -0
- package/dist/skills/skill-registry-service.js +92 -0
- package/dist/skills/skill-resolver.js +191 -0
- package/dist/workflows/command-allowlist-adapter.js +70 -0
- package/dist/workflows/schema.js +4 -0
- package/dist/workflows/workflow-executor.js +345 -0
- package/dist/workflows/workflow-registry.js +66 -0
- package/docs/architecture.md +698 -0
- package/docs/cli.md +741 -0
- package/docs/funcionamiento-del-sistema.md +868 -0
- package/docs/harness-gap-analysis.md +229 -0
- package/docs/prd.md +372 -0
- package/package.json +57 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
const finalStatuses = new Set(['completed', 'failed', 'blocked', 'cancelled']);
|
|
3
|
+
const allowedOutcomes = {
|
|
4
|
+
completed: ['success', 'partial'],
|
|
5
|
+
failed: ['failure'],
|
|
6
|
+
blocked: ['blocked'],
|
|
7
|
+
cancelled: ['cancelled'],
|
|
8
|
+
};
|
|
9
|
+
export class RunRepository {
|
|
10
|
+
db;
|
|
11
|
+
constructor(db) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
}
|
|
14
|
+
create(input) {
|
|
15
|
+
const validation = validateCreate(input);
|
|
16
|
+
if (!validation.ok)
|
|
17
|
+
return validation;
|
|
18
|
+
const result = this.db.transaction(() => {
|
|
19
|
+
const id = randomUUID();
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
this.db.connection.prepare(`
|
|
22
|
+
INSERT INTO runs(id, project, user_intent, workflow, phase, selected_agent_id, provider_adapter, model, status, created_at, updated_at)
|
|
23
|
+
VALUES (@id, @project, @userIntent, @workflow, @phase, @selectedAgentId, @providerAdapter, @model, 'created', @now, @now)
|
|
24
|
+
`).run({ ...input, id, now });
|
|
25
|
+
const run = this.getById(id);
|
|
26
|
+
if (!run.ok)
|
|
27
|
+
throw new Error(run.error.message);
|
|
28
|
+
return run.value;
|
|
29
|
+
});
|
|
30
|
+
return result.ok ? result : fail(result.error.message, result.error.cause, result.error.code);
|
|
31
|
+
}
|
|
32
|
+
getById(id) {
|
|
33
|
+
try {
|
|
34
|
+
const row = this.db.connection.prepare('SELECT * FROM runs WHERE id=?').get(id);
|
|
35
|
+
return row ? ok(mapRun(row)) : missing(`Run not found: ${id}`);
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
return fail('Failed to read run', cause);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
getDetails(id) {
|
|
42
|
+
const run = this.getById(id);
|
|
43
|
+
if (!run.ok)
|
|
44
|
+
return run;
|
|
45
|
+
const events = this.listEvents(id);
|
|
46
|
+
if (!events.ok)
|
|
47
|
+
return events;
|
|
48
|
+
const checkpoints = this.listCheckpoints(id);
|
|
49
|
+
if (!checkpoints.ok)
|
|
50
|
+
return checkpoints;
|
|
51
|
+
const approvals = this.listApprovals(id);
|
|
52
|
+
if (!approvals.ok)
|
|
53
|
+
return approvals;
|
|
54
|
+
const operationAttempts = this.listOperationAttempts(id);
|
|
55
|
+
if (!operationAttempts.ok)
|
|
56
|
+
return operationAttempts;
|
|
57
|
+
return ok({ ...run.value, events: events.value, checkpoints: checkpoints.value, approvals: approvals.value, operationAttempts: operationAttempts.value });
|
|
58
|
+
}
|
|
59
|
+
list(filters = {}) {
|
|
60
|
+
try {
|
|
61
|
+
const where = [];
|
|
62
|
+
const parameters = {};
|
|
63
|
+
if (filters.project !== undefined) {
|
|
64
|
+
where.push('project=@project');
|
|
65
|
+
parameters.project = filters.project;
|
|
66
|
+
}
|
|
67
|
+
if (filters.status !== undefined) {
|
|
68
|
+
where.push('status=@status');
|
|
69
|
+
parameters.status = filters.status;
|
|
70
|
+
}
|
|
71
|
+
const rows = this.db.connection.prepare(`
|
|
72
|
+
SELECT * FROM runs
|
|
73
|
+
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
74
|
+
ORDER BY created_at DESC
|
|
75
|
+
`).all(parameters);
|
|
76
|
+
return ok(rows.map(mapRun));
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
return fail('Failed to list runs', cause);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
appendEvent(input) {
|
|
83
|
+
const run = this.getById(input.runId);
|
|
84
|
+
if (!run.ok)
|
|
85
|
+
return run;
|
|
86
|
+
if (!input.title.trim())
|
|
87
|
+
return validationFailure('Run event title is required');
|
|
88
|
+
const result = this.db.transaction(() => {
|
|
89
|
+
const next = this.nextSequence('run_events', input.runId);
|
|
90
|
+
const event = {
|
|
91
|
+
id: randomUUID(),
|
|
92
|
+
runId: input.runId,
|
|
93
|
+
sequence: next,
|
|
94
|
+
kind: input.kind,
|
|
95
|
+
title: input.title,
|
|
96
|
+
payloadJson: JSON.stringify(input.payload ?? {}),
|
|
97
|
+
relatedType: input.relatedType ?? null,
|
|
98
|
+
relatedId: input.relatedId ?? null,
|
|
99
|
+
createdAt: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
this.db.connection.prepare(`
|
|
102
|
+
INSERT INTO run_events(id, run_id, sequence, kind, title, payload_json, related_type, related_id, created_at)
|
|
103
|
+
VALUES (@id, @runId, @sequence, @kind, @title, @payloadJson, @relatedType, @relatedId, @createdAt)
|
|
104
|
+
`).run(event);
|
|
105
|
+
return mapEventRow({ ...event, run_id: event.runId, payload_json: event.payloadJson, related_type: event.relatedType, related_id: event.relatedId, created_at: event.createdAt });
|
|
106
|
+
});
|
|
107
|
+
return result.ok ? result : fail(result.error.message, result.error.cause, result.error.code);
|
|
108
|
+
}
|
|
109
|
+
appendCheckpoint(input) {
|
|
110
|
+
const run = this.getById(input.runId);
|
|
111
|
+
if (!run.ok)
|
|
112
|
+
return run;
|
|
113
|
+
if (finalStatuses.has(run.value.status))
|
|
114
|
+
return validationFailure(`Cannot checkpoint terminal run: ${run.value.status}`);
|
|
115
|
+
if (!input.label.trim())
|
|
116
|
+
return validationFailure('Run checkpoint label is required');
|
|
117
|
+
const result = this.db.transaction(() => {
|
|
118
|
+
const next = this.nextSequence('run_checkpoints', input.runId);
|
|
119
|
+
const checkpoint = {
|
|
120
|
+
id: randomUUID(),
|
|
121
|
+
runId: input.runId,
|
|
122
|
+
sequence: next,
|
|
123
|
+
label: input.label,
|
|
124
|
+
stateJson: JSON.stringify(input.state),
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
this.db.connection.prepare(`
|
|
128
|
+
INSERT INTO run_checkpoints(id, run_id, sequence, label, state_json, created_at)
|
|
129
|
+
VALUES (@id, @runId, @sequence, @label, @stateJson, @createdAt)
|
|
130
|
+
`).run(checkpoint);
|
|
131
|
+
this.db.connection.prepare('UPDATE runs SET latest_checkpoint_id=@checkpointId, updated_at=@updatedAt WHERE id=@runId')
|
|
132
|
+
.run({ checkpointId: checkpoint.id, updatedAt: checkpoint.createdAt, runId: input.runId });
|
|
133
|
+
return mapCheckpointRow({ ...checkpoint, run_id: checkpoint.runId, state_json: checkpoint.stateJson, created_at: checkpoint.createdAt });
|
|
134
|
+
});
|
|
135
|
+
return result.ok ? result : fail(result.error.message, result.error.cause, result.error.code);
|
|
136
|
+
}
|
|
137
|
+
createApproval(input) {
|
|
138
|
+
const run = this.getById(input.runId);
|
|
139
|
+
if (!run.ok)
|
|
140
|
+
return run;
|
|
141
|
+
const decisionEvent = this.getEventById(input.decisionEventId);
|
|
142
|
+
if (!decisionEvent.ok)
|
|
143
|
+
return decisionEvent;
|
|
144
|
+
if (decisionEvent.value.runId !== input.runId)
|
|
145
|
+
return validationFailure('Approval decision event must belong to the run');
|
|
146
|
+
if (decisionEvent.value.kind !== 'permission-decision')
|
|
147
|
+
return validationFailure('Approval decision event must be a permission-decision event');
|
|
148
|
+
if (!isAskPermissionDecision(decisionEvent.value.payload))
|
|
149
|
+
return validationFailure('Approval can only be created for ask permission decisions');
|
|
150
|
+
const result = this.db.transaction(() => {
|
|
151
|
+
const approval = {
|
|
152
|
+
id: randomUUID(),
|
|
153
|
+
runId: input.runId,
|
|
154
|
+
decisionEventId: input.decisionEventId,
|
|
155
|
+
status: 'pending',
|
|
156
|
+
requestedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
this.db.connection.prepare(`
|
|
159
|
+
INSERT INTO run_approvals(id, run_id, decision_event_id, status, requested_at)
|
|
160
|
+
VALUES (@id, @runId, @decisionEventId, @status, @requestedAt)
|
|
161
|
+
`).run(approval);
|
|
162
|
+
this.db.connection.prepare('UPDATE runs SET updated_at=@updatedAt WHERE id=@runId')
|
|
163
|
+
.run({ runId: input.runId, updatedAt: approval.requestedAt });
|
|
164
|
+
return approval;
|
|
165
|
+
});
|
|
166
|
+
return result.ok ? result : fail(result.error.message, result.error.cause, result.error.code);
|
|
167
|
+
}
|
|
168
|
+
getApproval(id) {
|
|
169
|
+
try {
|
|
170
|
+
const row = this.db.connection.prepare('SELECT * FROM run_approvals WHERE id=?').get(id);
|
|
171
|
+
return row ? ok(mapApprovalRow(row)) : missing(`Approval not found: ${id}`);
|
|
172
|
+
}
|
|
173
|
+
catch (cause) {
|
|
174
|
+
return fail('Failed to read run approval', cause);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
listApprovals(runId) {
|
|
178
|
+
const run = this.getById(runId);
|
|
179
|
+
if (!run.ok)
|
|
180
|
+
return run;
|
|
181
|
+
try {
|
|
182
|
+
const rows = this.db.connection.prepare('SELECT * FROM run_approvals WHERE run_id=? ORDER BY requested_at ASC').all(runId);
|
|
183
|
+
return ok(rows.map(mapApprovalRow));
|
|
184
|
+
}
|
|
185
|
+
catch (cause) {
|
|
186
|
+
return fail('Failed to list run approvals', cause);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
listOperationAttempts(runId) {
|
|
190
|
+
const run = this.getById(runId);
|
|
191
|
+
if (!run.ok)
|
|
192
|
+
return run;
|
|
193
|
+
try {
|
|
194
|
+
const rows = this.db.connection.prepare('SELECT * FROM run_operation_attempts WHERE run_id=? ORDER BY COALESCE(approval_id, id) ASC, attempt_sequence ASC, reserved_at ASC').all(runId);
|
|
195
|
+
return ok(rows.map(mapOperationAttemptRow));
|
|
196
|
+
}
|
|
197
|
+
catch (cause) {
|
|
198
|
+
return fail('Failed to list run operation attempts', cause);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
getOperationAttempt(id) {
|
|
202
|
+
try {
|
|
203
|
+
const row = this.db.connection.prepare('SELECT * FROM run_operation_attempts WHERE id=?').get(id);
|
|
204
|
+
return row ? ok(mapOperationAttemptRow(row)) : missing(`Run operation attempt not found: ${id}`);
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
return fail('Failed to read run operation attempt', cause);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
reserveOperationAttempt(input) {
|
|
211
|
+
const run = this.getById(input.runId);
|
|
212
|
+
if (!run.ok)
|
|
213
|
+
return run;
|
|
214
|
+
const decisionEvent = this.getEventById(input.decisionEventId);
|
|
215
|
+
if (!decisionEvent.ok)
|
|
216
|
+
return decisionEvent;
|
|
217
|
+
if (decisionEvent.value.runId !== input.runId)
|
|
218
|
+
return validationFailure('Attempt decision event must belong to the run');
|
|
219
|
+
if (input.pendingExecutionEventId !== undefined) {
|
|
220
|
+
const pendingEvent = this.getEventById(input.pendingExecutionEventId);
|
|
221
|
+
if (!pendingEvent.ok)
|
|
222
|
+
return pendingEvent;
|
|
223
|
+
if (pendingEvent.value.runId !== input.runId)
|
|
224
|
+
return validationFailure('Attempt pending execution event must belong to the run');
|
|
225
|
+
}
|
|
226
|
+
const result = this.db.transaction(() => {
|
|
227
|
+
const now = new Date().toISOString();
|
|
228
|
+
const existingReserved = input.approvalId === undefined
|
|
229
|
+
? undefined
|
|
230
|
+
: this.db.connection.prepare("SELECT * FROM run_operation_attempts WHERE approval_id=? AND status='reserved'").get(input.approvalId);
|
|
231
|
+
if (existingReserved !== undefined) {
|
|
232
|
+
return validationFailure(`Approval already has an active operation attempt reservation: ${mapOperationAttemptRow(existingReserved).id}`);
|
|
233
|
+
}
|
|
234
|
+
const attemptSequence = input.approvalId === undefined
|
|
235
|
+
? 1
|
|
236
|
+
: this.db.connection.prepare('SELECT COALESCE(MAX(attempt_sequence), 0) + 1 AS nextSequence FROM run_operation_attempts WHERE approval_id=?').get(input.approvalId).nextSequence;
|
|
237
|
+
const attempt = {
|
|
238
|
+
id: randomUUID(),
|
|
239
|
+
runId: input.runId,
|
|
240
|
+
approvalId: input.approvalId ?? null,
|
|
241
|
+
attemptSequence,
|
|
242
|
+
decisionEventId: input.decisionEventId,
|
|
243
|
+
pendingExecutionEventId: input.pendingExecutionEventId ?? null,
|
|
244
|
+
category: input.category,
|
|
245
|
+
operation: input.operation,
|
|
246
|
+
operationJson: JSON.stringify(input.operationMetadata),
|
|
247
|
+
executorName: input.executorName,
|
|
248
|
+
status: 'reserved',
|
|
249
|
+
reservedAt: now,
|
|
250
|
+
updatedAt: now,
|
|
251
|
+
};
|
|
252
|
+
const insert = this.db.connection.prepare(`
|
|
253
|
+
INSERT OR IGNORE INTO run_operation_attempts(
|
|
254
|
+
id, run_id, approval_id, attempt_sequence, decision_event_id, pending_execution_event_id,
|
|
255
|
+
category, operation, operation_json, executor_name, status, reserved_at, updated_at
|
|
256
|
+
) VALUES (
|
|
257
|
+
@id, @runId, @approvalId, @attemptSequence, @decisionEventId, @pendingExecutionEventId,
|
|
258
|
+
@category, @operation, @operationJson, @executorName, @status, @reservedAt, @updatedAt
|
|
259
|
+
)
|
|
260
|
+
`).run(attempt);
|
|
261
|
+
if (insert.changes === 0) {
|
|
262
|
+
const existing = input.approvalId === undefined
|
|
263
|
+
? undefined
|
|
264
|
+
: this.db.connection.prepare('SELECT * FROM run_operation_attempts WHERE approval_id=? AND attempt_sequence=?').get(input.approvalId, attemptSequence);
|
|
265
|
+
const status = existing === undefined ? 'existing' : mapOperationAttemptRow(existing).status;
|
|
266
|
+
return validationFailure(`Approval attempt sequence is already reserved: ${status}`);
|
|
267
|
+
}
|
|
268
|
+
this.db.connection.prepare('UPDATE runs SET updated_at=@updatedAt WHERE id=@runId')
|
|
269
|
+
.run({ runId: input.runId, updatedAt: now });
|
|
270
|
+
return ok(mapOperationAttemptRow({
|
|
271
|
+
...attempt,
|
|
272
|
+
run_id: attempt.runId,
|
|
273
|
+
approval_id: attempt.approvalId,
|
|
274
|
+
attempt_sequence: attempt.attemptSequence,
|
|
275
|
+
decision_event_id: attempt.decisionEventId,
|
|
276
|
+
pending_execution_event_id: attempt.pendingExecutionEventId,
|
|
277
|
+
operation_json: attempt.operationJson,
|
|
278
|
+
executor_name: attempt.executorName,
|
|
279
|
+
result_event_id: null,
|
|
280
|
+
output_json: null,
|
|
281
|
+
error_json: null,
|
|
282
|
+
reserved_at: attempt.reservedAt,
|
|
283
|
+
updated_at: attempt.updatedAt,
|
|
284
|
+
completed_at: null,
|
|
285
|
+
}));
|
|
286
|
+
});
|
|
287
|
+
return result.ok ? result.value : fail(result.error.message, result.error.cause, result.error.code);
|
|
288
|
+
}
|
|
289
|
+
completeOperationAttempt(input) {
|
|
290
|
+
const existing = this.getOperationAttempt(input.attemptId);
|
|
291
|
+
if (!existing.ok)
|
|
292
|
+
return existing;
|
|
293
|
+
if (existing.value.status !== 'reserved')
|
|
294
|
+
return validationFailure(`Operation attempt is already finalized: ${existing.value.status}`);
|
|
295
|
+
const result = this.db.transaction(() => {
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
this.db.connection.prepare(`
|
|
298
|
+
UPDATE run_operation_attempts
|
|
299
|
+
SET status=@status, output_json=@outputJson, error_json=@errorJson, updated_at=@now, completed_at=@now
|
|
300
|
+
WHERE id=@attemptId AND status='reserved'
|
|
301
|
+
`).run({
|
|
302
|
+
attemptId: input.attemptId,
|
|
303
|
+
status: input.status,
|
|
304
|
+
outputJson: input.output === undefined ? null : JSON.stringify(input.output),
|
|
305
|
+
errorJson: input.error === undefined ? null : JSON.stringify(input.error),
|
|
306
|
+
now,
|
|
307
|
+
});
|
|
308
|
+
this.db.connection.prepare('UPDATE runs SET updated_at=@updatedAt WHERE id=@runId')
|
|
309
|
+
.run({ runId: existing.value.runId, updatedAt: now });
|
|
310
|
+
const updated = this.getOperationAttempt(input.attemptId);
|
|
311
|
+
if (!updated.ok)
|
|
312
|
+
throw new Error(updated.error.message);
|
|
313
|
+
return updated.value;
|
|
314
|
+
});
|
|
315
|
+
return result.ok ? result : fail(result.error.message, result.error.cause, result.error.code);
|
|
316
|
+
}
|
|
317
|
+
abandonOperationAttempt(input) {
|
|
318
|
+
const existing = this.getOperationAttempt(input.attemptId);
|
|
319
|
+
if (!existing.ok)
|
|
320
|
+
return existing;
|
|
321
|
+
if (existing.value.status !== 'reserved')
|
|
322
|
+
return validationFailure(`Only reserved operation attempts can be abandoned: ${existing.value.status}`);
|
|
323
|
+
if (!input.actor.trim())
|
|
324
|
+
return validationFailure('Operation attempt abandonment actor is required');
|
|
325
|
+
if (!input.reason.trim())
|
|
326
|
+
return validationFailure('Operation attempt abandonment reason is required');
|
|
327
|
+
const result = this.db.transaction(() => {
|
|
328
|
+
const now = new Date().toISOString();
|
|
329
|
+
const update = this.db.connection.prepare(`
|
|
330
|
+
UPDATE run_operation_attempts
|
|
331
|
+
SET status='abandoned', updated_at=@now, completed_at=@now
|
|
332
|
+
WHERE id=@attemptId AND status='reserved'
|
|
333
|
+
`).run({ attemptId: input.attemptId, now });
|
|
334
|
+
if (update.changes === 0)
|
|
335
|
+
return validationFailure(`Operation attempt is already finalized: ${input.attemptId}`);
|
|
336
|
+
const event = this.insertEvent({
|
|
337
|
+
runId: existing.value.runId,
|
|
338
|
+
kind: 'operation-execution',
|
|
339
|
+
title: `Operation attempt abandoned: ${existing.value.category} ${existing.value.operation}`,
|
|
340
|
+
payload: {
|
|
341
|
+
status: 'abandoned',
|
|
342
|
+
attemptId: existing.value.id,
|
|
343
|
+
approvalId: existing.value.approvalId ?? null,
|
|
344
|
+
decisionEventId: existing.value.decisionEventId,
|
|
345
|
+
pendingExecutionEventId: existing.value.pendingExecutionEventId ?? null,
|
|
346
|
+
actor: input.actor,
|
|
347
|
+
reason: input.reason,
|
|
348
|
+
timestamp: now,
|
|
349
|
+
executorName: existing.value.executorName,
|
|
350
|
+
executorInvoked: false,
|
|
351
|
+
operationExecuted: false,
|
|
352
|
+
retryAllowed: false,
|
|
353
|
+
requestedOperation: existing.value.operationMetadata,
|
|
354
|
+
},
|
|
355
|
+
relatedType: 'operation-attempt',
|
|
356
|
+
relatedId: existing.value.id,
|
|
357
|
+
});
|
|
358
|
+
this.db.connection.prepare('UPDATE run_operation_attempts SET result_event_id=@eventId, updated_at=@now WHERE id=@attemptId')
|
|
359
|
+
.run({ attemptId: input.attemptId, eventId: event.id, now });
|
|
360
|
+
this.db.connection.prepare('UPDATE runs SET updated_at=@updatedAt WHERE id=@runId')
|
|
361
|
+
.run({ runId: existing.value.runId, updatedAt: now });
|
|
362
|
+
const updated = this.getOperationAttempt(input.attemptId);
|
|
363
|
+
if (!updated.ok)
|
|
364
|
+
throw new Error(updated.error.message);
|
|
365
|
+
return ok({ attempt: updated.value, event });
|
|
366
|
+
});
|
|
367
|
+
return result.ok ? result.value : fail(result.error.message, result.error.cause, result.error.code);
|
|
368
|
+
}
|
|
369
|
+
attachAttemptResultEvent(attemptId, eventId) {
|
|
370
|
+
const attempt = this.getOperationAttempt(attemptId);
|
|
371
|
+
if (!attempt.ok)
|
|
372
|
+
return attempt;
|
|
373
|
+
const event = this.getEventById(eventId);
|
|
374
|
+
if (!event.ok)
|
|
375
|
+
return event;
|
|
376
|
+
if (event.value.runId !== attempt.value.runId)
|
|
377
|
+
return validationFailure('Attempt result event must belong to the run');
|
|
378
|
+
const result = this.db.transaction(() => {
|
|
379
|
+
const now = new Date().toISOString();
|
|
380
|
+
this.db.connection.prepare('UPDATE run_operation_attempts SET result_event_id=@eventId, updated_at=@now WHERE id=@attemptId')
|
|
381
|
+
.run({ attemptId, eventId, now });
|
|
382
|
+
const updated = this.getOperationAttempt(attemptId);
|
|
383
|
+
if (!updated.ok)
|
|
384
|
+
throw new Error(updated.error.message);
|
|
385
|
+
return updated.value;
|
|
386
|
+
});
|
|
387
|
+
return result.ok ? result : fail(result.error.message, result.error.cause, result.error.code);
|
|
388
|
+
}
|
|
389
|
+
resolveApproval(input) {
|
|
390
|
+
const approval = this.getApproval(input.approvalId);
|
|
391
|
+
if (!approval.ok)
|
|
392
|
+
return approval;
|
|
393
|
+
if (approval.value.status !== 'pending')
|
|
394
|
+
return validationFailure(`Approval is already resolved: ${approval.value.status}`);
|
|
395
|
+
if (!input.actor.trim())
|
|
396
|
+
return validationFailure('Approval resolution actor is required');
|
|
397
|
+
const result = this.db.transaction(() => {
|
|
398
|
+
const resolvedAt = new Date().toISOString();
|
|
399
|
+
const update = this.db.connection.prepare(`
|
|
400
|
+
UPDATE run_approvals
|
|
401
|
+
SET status=@status, resolved_at=@resolvedAt, resolved_by=@actor, resolution_reason=@reason
|
|
402
|
+
WHERE id=@approvalId AND status='pending'
|
|
403
|
+
`).run({ approvalId: input.approvalId, status: input.status, resolvedAt, actor: input.actor, reason: input.reason ?? null });
|
|
404
|
+
if (update.changes === 0)
|
|
405
|
+
return validationFailure(`Approval is already resolved or changed by another resolver: ${input.approvalId}`);
|
|
406
|
+
const event = this.insertEvent({
|
|
407
|
+
runId: approval.value.runId,
|
|
408
|
+
kind: 'approval',
|
|
409
|
+
title: `Approval ${input.status}`,
|
|
410
|
+
payload: {
|
|
411
|
+
approvalId: input.approvalId,
|
|
412
|
+
decisionEventId: approval.value.decisionEventId,
|
|
413
|
+
status: input.status,
|
|
414
|
+
actor: input.actor,
|
|
415
|
+
reason: input.reason ?? null,
|
|
416
|
+
resolvedAt,
|
|
417
|
+
operationExecuted: false,
|
|
418
|
+
},
|
|
419
|
+
relatedType: 'approval',
|
|
420
|
+
relatedId: input.approvalId,
|
|
421
|
+
});
|
|
422
|
+
this.db.connection.prepare('UPDATE runs SET updated_at=@updatedAt WHERE id=@runId')
|
|
423
|
+
.run({ runId: approval.value.runId, updatedAt: resolvedAt });
|
|
424
|
+
const resolved = this.getApproval(input.approvalId);
|
|
425
|
+
if (!resolved.ok)
|
|
426
|
+
throw new Error(resolved.error.message);
|
|
427
|
+
return ok({ approval: resolved.value, event });
|
|
428
|
+
});
|
|
429
|
+
return result.ok ? result.value : fail(result.error.message, result.error.cause, result.error.code);
|
|
430
|
+
}
|
|
431
|
+
updateFinalStatus(input) {
|
|
432
|
+
const run = this.getById(input.runId);
|
|
433
|
+
if (!run.ok)
|
|
434
|
+
return run;
|
|
435
|
+
if (finalStatuses.has(run.value.status))
|
|
436
|
+
return validationFailure(`Run is already terminal: ${run.value.status}`);
|
|
437
|
+
if (!allowedOutcomes[input.status].includes(input.outcome))
|
|
438
|
+
return validationFailure(`Outcome ${input.outcome} is not valid for status ${input.status}`);
|
|
439
|
+
const result = this.db.transaction(() => {
|
|
440
|
+
const now = new Date().toISOString();
|
|
441
|
+
this.db.connection.prepare(`
|
|
442
|
+
UPDATE runs
|
|
443
|
+
SET status=@status, outcome=@outcome, outcome_reason=@outcomeReason, completed_at=@now, updated_at=@now
|
|
444
|
+
WHERE id=@runId
|
|
445
|
+
`).run({ ...input, outcomeReason: input.outcomeReason ?? null, now });
|
|
446
|
+
const updated = this.getById(input.runId);
|
|
447
|
+
if (!updated.ok)
|
|
448
|
+
throw new Error(updated.error.message);
|
|
449
|
+
return updated.value;
|
|
450
|
+
});
|
|
451
|
+
return result.ok ? result : fail(result.error.message, result.error.cause, result.error.code);
|
|
452
|
+
}
|
|
453
|
+
listEvents(runId) {
|
|
454
|
+
try {
|
|
455
|
+
const rows = this.db.connection.prepare('SELECT * FROM run_events WHERE run_id=? ORDER BY sequence ASC').all(runId);
|
|
456
|
+
return ok(rows.map(mapEventRow));
|
|
457
|
+
}
|
|
458
|
+
catch (cause) {
|
|
459
|
+
return fail('Failed to list run events', cause);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
getEventById(id) {
|
|
463
|
+
try {
|
|
464
|
+
const row = this.db.connection.prepare('SELECT * FROM run_events WHERE id=?').get(id);
|
|
465
|
+
return row ? ok(mapEventRow(row)) : missing(`Run event not found: ${id}`);
|
|
466
|
+
}
|
|
467
|
+
catch (cause) {
|
|
468
|
+
return fail('Failed to read run event', cause);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
listCheckpoints(runId) {
|
|
472
|
+
try {
|
|
473
|
+
const rows = this.db.connection.prepare('SELECT * FROM run_checkpoints WHERE run_id=? ORDER BY sequence ASC').all(runId);
|
|
474
|
+
return ok(rows.map(mapCheckpointRow));
|
|
475
|
+
}
|
|
476
|
+
catch (cause) {
|
|
477
|
+
return fail('Failed to list run checkpoints', cause);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
nextSequence(table, runId) {
|
|
481
|
+
const row = this.db.connection.prepare(`SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence FROM ${table} WHERE run_id=?`).get(runId);
|
|
482
|
+
return row.sequence;
|
|
483
|
+
}
|
|
484
|
+
insertEvent(input) {
|
|
485
|
+
const next = this.nextSequence('run_events', input.runId);
|
|
486
|
+
const event = {
|
|
487
|
+
id: randomUUID(),
|
|
488
|
+
runId: input.runId,
|
|
489
|
+
sequence: next,
|
|
490
|
+
kind: input.kind,
|
|
491
|
+
title: input.title,
|
|
492
|
+
payloadJson: JSON.stringify(input.payload ?? {}),
|
|
493
|
+
relatedType: input.relatedType ?? null,
|
|
494
|
+
relatedId: input.relatedId ?? null,
|
|
495
|
+
createdAt: new Date().toISOString(),
|
|
496
|
+
};
|
|
497
|
+
this.db.connection.prepare(`
|
|
498
|
+
INSERT INTO run_events(id, run_id, sequence, kind, title, payload_json, related_type, related_id, created_at)
|
|
499
|
+
VALUES (@id, @runId, @sequence, @kind, @title, @payloadJson, @relatedType, @relatedId, @createdAt)
|
|
500
|
+
`).run(event);
|
|
501
|
+
return mapEventRow({ ...event, run_id: event.runId, payload_json: event.payloadJson, related_type: event.relatedType, related_id: event.relatedId, created_at: event.createdAt });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function validateCreate(input) {
|
|
505
|
+
if (!input.project.trim())
|
|
506
|
+
return validationFailure('Run project is required');
|
|
507
|
+
if (!input.userIntent.trim())
|
|
508
|
+
return validationFailure('Run user intent is required');
|
|
509
|
+
if (!input.workflow.trim())
|
|
510
|
+
return validationFailure('Run workflow is required');
|
|
511
|
+
if (!input.phase.trim())
|
|
512
|
+
return validationFailure('Run phase is required');
|
|
513
|
+
if (!input.selectedAgentId.trim())
|
|
514
|
+
return validationFailure('Run selectedAgentId is required');
|
|
515
|
+
if (!input.providerAdapter.trim())
|
|
516
|
+
return validationFailure('Run providerAdapter is required');
|
|
517
|
+
if (!input.model.trim())
|
|
518
|
+
return validationFailure('Run model is required');
|
|
519
|
+
return ok(undefined);
|
|
520
|
+
}
|
|
521
|
+
function mapRun(row) {
|
|
522
|
+
const run = {
|
|
523
|
+
id: row.id,
|
|
524
|
+
project: row.project,
|
|
525
|
+
userIntent: row.user_intent,
|
|
526
|
+
workflow: row.workflow,
|
|
527
|
+
phase: row.phase,
|
|
528
|
+
selectedAgentId: row.selected_agent_id,
|
|
529
|
+
providerAdapter: row.provider_adapter,
|
|
530
|
+
model: row.model,
|
|
531
|
+
status: row.status,
|
|
532
|
+
createdAt: row.created_at,
|
|
533
|
+
updatedAt: row.updated_at,
|
|
534
|
+
};
|
|
535
|
+
if (row.outcome !== null)
|
|
536
|
+
run.outcome = row.outcome;
|
|
537
|
+
if (row.outcome_reason !== null)
|
|
538
|
+
run.outcomeReason = row.outcome_reason;
|
|
539
|
+
if (row.latest_checkpoint_id !== null)
|
|
540
|
+
run.latestCheckpointId = row.latest_checkpoint_id;
|
|
541
|
+
if (row.completed_at !== null)
|
|
542
|
+
run.completedAt = row.completed_at;
|
|
543
|
+
return run;
|
|
544
|
+
}
|
|
545
|
+
function mapEventRow(row) {
|
|
546
|
+
const event = { id: row.id, runId: row.run_id, sequence: row.sequence, kind: row.kind, title: row.title, payload: JSON.parse(row.payload_json), createdAt: row.created_at };
|
|
547
|
+
if (row.related_type !== null)
|
|
548
|
+
event.relatedType = row.related_type;
|
|
549
|
+
if (row.related_id !== null)
|
|
550
|
+
event.relatedId = row.related_id;
|
|
551
|
+
return event;
|
|
552
|
+
}
|
|
553
|
+
function mapCheckpointRow(row) {
|
|
554
|
+
return { id: row.id, runId: row.run_id, sequence: row.sequence, label: row.label, state: JSON.parse(row.state_json), createdAt: row.created_at };
|
|
555
|
+
}
|
|
556
|
+
function mapApprovalRow(row) {
|
|
557
|
+
const approval = {
|
|
558
|
+
id: row.id,
|
|
559
|
+
runId: row.run_id,
|
|
560
|
+
decisionEventId: row.decision_event_id,
|
|
561
|
+
status: row.status,
|
|
562
|
+
requestedAt: row.requested_at,
|
|
563
|
+
};
|
|
564
|
+
if (row.resolved_at !== null)
|
|
565
|
+
approval.resolvedAt = row.resolved_at;
|
|
566
|
+
if (row.resolved_by !== null)
|
|
567
|
+
approval.resolvedBy = row.resolved_by;
|
|
568
|
+
if (row.resolution_reason !== null)
|
|
569
|
+
approval.resolutionReason = row.resolution_reason;
|
|
570
|
+
return approval;
|
|
571
|
+
}
|
|
572
|
+
function mapOperationAttemptRow(row) {
|
|
573
|
+
const attempt = {
|
|
574
|
+
id: row.id,
|
|
575
|
+
runId: row.run_id,
|
|
576
|
+
attemptSequence: row.attempt_sequence ?? 1,
|
|
577
|
+
decisionEventId: row.decision_event_id,
|
|
578
|
+
category: row.category,
|
|
579
|
+
operation: row.operation,
|
|
580
|
+
operationMetadata: JSON.parse(row.operation_json),
|
|
581
|
+
executorName: row.executor_name,
|
|
582
|
+
status: row.status,
|
|
583
|
+
reservedAt: row.reserved_at,
|
|
584
|
+
updatedAt: row.updated_at,
|
|
585
|
+
};
|
|
586
|
+
if (row.approval_id !== null)
|
|
587
|
+
attempt.approvalId = row.approval_id;
|
|
588
|
+
if (row.pending_execution_event_id !== null)
|
|
589
|
+
attempt.pendingExecutionEventId = row.pending_execution_event_id;
|
|
590
|
+
if (row.result_event_id !== null)
|
|
591
|
+
attempt.resultEventId = row.result_event_id;
|
|
592
|
+
if (row.output_json !== null)
|
|
593
|
+
attempt.output = JSON.parse(row.output_json);
|
|
594
|
+
if (row.error_json !== null)
|
|
595
|
+
attempt.error = JSON.parse(row.error_json);
|
|
596
|
+
if (row.completed_at !== null)
|
|
597
|
+
attempt.completedAt = row.completed_at;
|
|
598
|
+
return attempt;
|
|
599
|
+
}
|
|
600
|
+
function isAskPermissionDecision(payload) {
|
|
601
|
+
return typeof payload === 'object' && payload !== null && !Array.isArray(payload) && payload.decision === 'ask';
|
|
602
|
+
}
|
|
603
|
+
function ok(value) { return { ok: true, value }; }
|
|
604
|
+
function missing(message) { return { ok: false, error: { code: 'not_found', message } }; }
|
|
605
|
+
function validationFailure(message) { return { ok: false, error: { code: 'validation_failed', message } }; }
|
|
606
|
+
function fail(message, cause, code = 'validation_failed') {
|
|
607
|
+
const error = { code, message };
|
|
608
|
+
if (cause !== undefined)
|
|
609
|
+
error.cause = cause;
|
|
610
|
+
return { ok: false, error };
|
|
611
|
+
}
|