agent-mockingbird 0.0.1
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/.agents/skills/btca-cli/SKILL.md +64 -0
- package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
- package/.env.example +36 -0
- package/.githooks/pre-commit +33 -0
- package/.github/workflows/ci.yml +309 -0
- package/.opencode/bun.lock +18 -0
- package/.opencode/package.json +5 -0
- package/.opencode/tools/agent_type_manager.ts +100 -0
- package/.opencode/tools/config_manager.ts +87 -0
- package/.opencode/tools/cron_manager.ts +145 -0
- package/.opencode/tools/memory_get.ts +43 -0
- package/.opencode/tools/memory_remember.ts +53 -0
- package/.opencode/tools/memory_search.ts +48 -0
- package/AGENTS.md +126 -0
- package/MEMORY.md +2 -0
- package/README.md +451 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/agent-mockingbird.config.example.json +135 -0
- package/apps/server/package.json +32 -0
- package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
- package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
- package/apps/server/src/backend/agents/openclawImport.ts +797 -0
- package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
- package/apps/server/src/backend/agents/service.ts +10 -0
- package/apps/server/src/backend/config/example-config.test.ts +20 -0
- package/apps/server/src/backend/config/orchestration.ts +243 -0
- package/apps/server/src/backend/config/policy.ts +158 -0
- package/apps/server/src/backend/config/schema.test.ts +15 -0
- package/apps/server/src/backend/config/schema.ts +391 -0
- package/apps/server/src/backend/config/semantic.test.ts +34 -0
- package/apps/server/src/backend/config/semantic.ts +149 -0
- package/apps/server/src/backend/config/service.test.ts +75 -0
- package/apps/server/src/backend/config/service.ts +207 -0
- package/apps/server/src/backend/config/smoke.ts +77 -0
- package/apps/server/src/backend/config/store.test.ts +123 -0
- package/apps/server/src/backend/config/store.ts +581 -0
- package/apps/server/src/backend/config/testFixtures.ts +5 -0
- package/apps/server/src/backend/config/types.ts +56 -0
- package/apps/server/src/backend/contracts/events.ts +320 -0
- package/apps/server/src/backend/contracts/runtime.ts +111 -0
- package/apps/server/src/backend/cron/executor.ts +435 -0
- package/apps/server/src/backend/cron/repository.ts +170 -0
- package/apps/server/src/backend/cron/service.ts +660 -0
- package/apps/server/src/backend/cron/storage.ts +92 -0
- package/apps/server/src/backend/cron/types.ts +138 -0
- package/apps/server/src/backend/cron/utils.ts +351 -0
- package/apps/server/src/backend/db/client.ts +20 -0
- package/apps/server/src/backend/db/migrate.ts +40 -0
- package/apps/server/src/backend/db/repository.ts +1762 -0
- package/apps/server/src/backend/db/schema.ts +113 -0
- package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
- package/apps/server/src/backend/db/wipe.ts +13 -0
- package/apps/server/src/backend/defaults.ts +32 -0
- package/apps/server/src/backend/env.ts +48 -0
- package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
- package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
- package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
- package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
- package/apps/server/src/backend/heartbeat/service.ts +176 -0
- package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
- package/apps/server/src/backend/heartbeat/state.ts +167 -0
- package/apps/server/src/backend/heartbeat/types.ts +54 -0
- package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
- package/apps/server/src/backend/http/boundedQueue.ts +92 -0
- package/apps/server/src/backend/http/parsers.ts +40 -0
- package/apps/server/src/backend/http/router.ts +61 -0
- package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
- package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
- package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
- package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
- package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
- package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
- package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
- package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
- package/apps/server/src/backend/http/routes/index.ts +101 -0
- package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
- package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
- package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
- package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
- package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
- package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
- package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
- package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
- package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
- package/apps/server/src/backend/http/schemas.ts +64 -0
- package/apps/server/src/backend/http/sse.ts +144 -0
- package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
- package/apps/server/src/backend/logging/logger.ts +64 -0
- package/apps/server/src/backend/mcp/service.ts +326 -0
- package/apps/server/src/backend/memory/cli.ts +170 -0
- package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
- package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
- package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
- package/apps/server/src/backend/memory/qmdPort.ts +61 -0
- package/apps/server/src/backend/memory/records.test.ts +66 -0
- package/apps/server/src/backend/memory/records.ts +229 -0
- package/apps/server/src/backend/memory/service.ts +2012 -0
- package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
- package/apps/server/src/backend/memory/types.ts +104 -0
- package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
- package/apps/server/src/backend/opencode/client.ts +98 -0
- package/apps/server/src/backend/opencode/models.ts +41 -0
- package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
- package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
- package/apps/server/src/backend/paths.ts +57 -0
- package/apps/server/src/backend/prompts/service.ts +100 -0
- package/apps/server/src/backend/queue/queue.test.ts +189 -0
- package/apps/server/src/backend/queue/service.ts +177 -0
- package/apps/server/src/backend/queue/types.ts +39 -0
- package/apps/server/src/backend/run/service.ts +576 -0
- package/apps/server/src/backend/run/storage.ts +47 -0
- package/apps/server/src/backend/run/types.ts +44 -0
- package/apps/server/src/backend/runtime/errors.ts +61 -0
- package/apps/server/src/backend/runtime/index.ts +72 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
- package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
- package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
- package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
- package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
- package/apps/server/src/backend/skills/service.ts +442 -0
- package/apps/server/src/backend/workspace/resolve.ts +27 -0
- package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
- package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
- package/apps/server/src/cli/runtime-assets.mjs +269 -0
- package/apps/server/src/cli/runtime-assets.test.ts +52 -0
- package/apps/server/src/cli/runtime-layout.mjs +75 -0
- package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
- package/apps/server/src/cli/standaloneBuild.ts +19 -0
- package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
- package/apps/server/src/index.ts +178 -0
- package/apps/server/tsconfig.json +12 -0
- package/backlog.md +5 -0
- package/bin/agent-mockingbird +2522 -0
- package/bin/runtime-layout.mjs +75 -0
- package/build-bin.ts +34 -0
- package/build-cli.mjs +37 -0
- package/build.ts +40 -0
- package/bun-env.d.ts +11 -0
- package/bun.lock +888 -0
- package/bunfig.toml +2 -0
- package/components.json +21 -0
- package/config.json +130 -0
- package/deploy/RELEASE_INSTALL.md +112 -0
- package/deploy/docker-compose.yml +42 -0
- package/deploy/systemd/README.md +46 -0
- package/deploy/systemd/agent-mockingbird.service +28 -0
- package/deploy/systemd/opencode.service +25 -0
- package/docs/legacy-config-ui-reference.md +51 -0
- package/docs/memory-e2e-trace-2026-03-04.md +63 -0
- package/docs/memory-ops.md +96 -0
- package/docs/memory-runtime-contract.md +42 -0
- package/docs/memory-tuning-remote-2026-03-04.md +59 -0
- package/docs/opencode-rebase-workflow-plan.md +614 -0
- package/docs/opencode-startup-sync-plan.md +94 -0
- package/docs/vendor-opencode.md +41 -0
- package/drizzle/0000_famous_turbo.sql +49 -0
- package/drizzle/0001_cron_memory_aux.sql +160 -0
- package/drizzle/0002_runtime_session_bindings.sql +28 -0
- package/drizzle/0003_background_runs.sql +27 -0
- package/drizzle/0004_memory_open_write.sql +63 -0
- package/drizzle/0005_signal_channel.sql +47 -0
- package/drizzle/0006_usage_event_dimensions.sql +7 -0
- package/drizzle/meta/0000_snapshot.json +341 -0
- package/drizzle/meta/_journal.json +55 -0
- package/drizzle.config.ts +14 -0
- package/eslint.config.mjs +77 -0
- package/knip.json +18 -0
- package/memory/2026-03-04.md +4 -0
- package/opencode.lock.json +16 -0
- package/package.json +67 -0
- package/packages/agent-mockingbird-installer/README.md +31 -0
- package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
- package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
- package/packages/agent-mockingbird-installer/package.json +23 -0
- package/packages/contracts/package.json +19 -0
- package/packages/contracts/src/agentTypes.ts +122 -0
- package/packages/contracts/src/cron.ts +146 -0
- package/packages/contracts/src/dashboard.ts +378 -0
- package/packages/contracts/src/index.ts +3 -0
- package/packages/contracts/tsconfig.json +4 -0
- package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
- package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
- package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
- package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
- package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
- package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
- package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
- package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
- package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
- package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
- package/runtime-assets/opencode-config/opencode.jsonc +25 -0
- package/runtime-assets/opencode-config/package.json +5 -0
- package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
- package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
- package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
- package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
- package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
- package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
- package/runtime-assets/workspace/AGENTS.md +56 -0
- package/runtime-assets/workspace/MEMORY.md +4 -0
- package/scripts/build-release-bundle.sh +66 -0
- package/scripts/check-ship.ts +383 -0
- package/scripts/dev-opencode.sh +17 -0
- package/scripts/dev-stack-opencode.sh +15 -0
- package/scripts/dev-stack.sh +61 -0
- package/scripts/install-systemd.sh +87 -0
- package/scripts/memory-e2e.sh +76 -0
- package/scripts/memory-trace-e2e.sh +141 -0
- package/scripts/migrate-opencode-env.ts +108 -0
- package/scripts/onboard/bootstrap.sh +32 -0
- package/scripts/opencode-swap.ts +78 -0
- package/scripts/opencode-sync.ts +715 -0
- package/scripts/runtime-assets-sync.mjs +83 -0
- package/scripts/setup-git-hooks.ts +39 -0
- package/tsconfig.json +45 -0
- package/tui.json +98 -0
- package/turbo.json +36 -0
- package/vendor/OPENCODE_VENDOR.md +13 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { ensureRunTables } from "./storage";
|
|
2
|
+
import type { AgentRun, AgentRunEvent, AgentRunEventType, CreateAgentRunInput } from "./types";
|
|
3
|
+
import type { RuntimeInputPart , RuntimeEngine } from "../contracts/runtime";
|
|
4
|
+
import { sqlite } from "../db/client";
|
|
5
|
+
import { getSessionById } from "../db/repository";
|
|
6
|
+
import { RuntimeContinuationDetachedError, RuntimeSessionQueuedError } from "../runtime/errors";
|
|
7
|
+
|
|
8
|
+
interface AgentRunRow {
|
|
9
|
+
id: string;
|
|
10
|
+
session_id: string;
|
|
11
|
+
state: AgentRun["state"];
|
|
12
|
+
content: string;
|
|
13
|
+
metadata_json: string;
|
|
14
|
+
idempotency_key: string | null;
|
|
15
|
+
result_json: string | null;
|
|
16
|
+
error_json: string | null;
|
|
17
|
+
created_at: number;
|
|
18
|
+
updated_at: number;
|
|
19
|
+
started_at: number | null;
|
|
20
|
+
completed_at: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AgentRunEventRow {
|
|
24
|
+
id: string;
|
|
25
|
+
run_id: string;
|
|
26
|
+
seq: number;
|
|
27
|
+
type: AgentRunEventType;
|
|
28
|
+
payload_json: string;
|
|
29
|
+
created_at: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const nowMs = () => Date.now();
|
|
33
|
+
const toIso = (ms: number) => new Date(ms).toISOString();
|
|
34
|
+
const RUN_ID_PREFIX = "run";
|
|
35
|
+
const RUN_PARTS_METADATA_KEY = "__inputParts";
|
|
36
|
+
type RunEventListener = (event: AgentRunEvent) => void;
|
|
37
|
+
|
|
38
|
+
function parseJson(value: string | null): unknown {
|
|
39
|
+
if (!value) return null;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(value) as unknown;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeMetadata(value: unknown): Record<string, unknown> {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
49
|
+
return value as Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeRuntimeInputParts(value: unknown): RuntimeInputPart[] {
|
|
53
|
+
if (!Array.isArray(value)) return [];
|
|
54
|
+
const parts: RuntimeInputPart[] = [];
|
|
55
|
+
for (const raw of value) {
|
|
56
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
57
|
+
const record = raw as Record<string, unknown>;
|
|
58
|
+
if (record.type === "text") {
|
|
59
|
+
const text = typeof record.text === "string" ? record.text : "";
|
|
60
|
+
if (!text.trim()) continue;
|
|
61
|
+
parts.push({
|
|
62
|
+
type: "text",
|
|
63
|
+
text,
|
|
64
|
+
});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (record.type === "file") {
|
|
68
|
+
const mime = typeof record.mime === "string" ? record.mime.trim() : "";
|
|
69
|
+
const url = typeof record.url === "string" ? record.url.trim() : "";
|
|
70
|
+
const filename = typeof record.filename === "string" ? record.filename.trim() || undefined : undefined;
|
|
71
|
+
if (!mime || !url) continue;
|
|
72
|
+
parts.push({
|
|
73
|
+
type: "file",
|
|
74
|
+
mime,
|
|
75
|
+
filename,
|
|
76
|
+
url,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return parts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rowToRun(row: AgentRunRow): AgentRun {
|
|
84
|
+
const metadata = normalizeMetadata(parseJson(row.metadata_json));
|
|
85
|
+
const parts = normalizeRuntimeInputParts(metadata[RUN_PARTS_METADATA_KEY]);
|
|
86
|
+
delete metadata[RUN_PARTS_METADATA_KEY];
|
|
87
|
+
return {
|
|
88
|
+
id: row.id,
|
|
89
|
+
sessionId: row.session_id,
|
|
90
|
+
state: row.state,
|
|
91
|
+
content: row.content,
|
|
92
|
+
parts: parts.length > 0 ? parts : undefined,
|
|
93
|
+
metadata,
|
|
94
|
+
idempotencyKey: row.idempotency_key,
|
|
95
|
+
result: parseJson(row.result_json),
|
|
96
|
+
error: parseJson(row.error_json),
|
|
97
|
+
createdAt: toIso(row.created_at),
|
|
98
|
+
updatedAt: toIso(row.updated_at),
|
|
99
|
+
startedAt: row.started_at ? toIso(row.started_at) : null,
|
|
100
|
+
completedAt: row.completed_at ? toIso(row.completed_at) : null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function rowToRunEvent(row: AgentRunEventRow): AgentRunEvent {
|
|
105
|
+
return {
|
|
106
|
+
id: row.id,
|
|
107
|
+
runId: row.run_id,
|
|
108
|
+
seq: row.seq,
|
|
109
|
+
type: row.type,
|
|
110
|
+
payload: parseJson(row.payload_json),
|
|
111
|
+
at: toIso(row.created_at),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createRunId() {
|
|
116
|
+
return `${RUN_ID_PREFIX}-${crypto.randomUUID().slice(0, 12)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function safeJson(value: unknown) {
|
|
120
|
+
return JSON.stringify(value ?? {});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function nextRunEventSeq(runId: string) {
|
|
124
|
+
const row = sqlite
|
|
125
|
+
.query(
|
|
126
|
+
`
|
|
127
|
+
SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq
|
|
128
|
+
FROM agent_run_events
|
|
129
|
+
WHERE run_id = ?1
|
|
130
|
+
`,
|
|
131
|
+
)
|
|
132
|
+
.get(runId) as { next_seq: number };
|
|
133
|
+
return row.next_seq;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class RunService {
|
|
137
|
+
private dispatchInFlight = false;
|
|
138
|
+
private recoveredPendingRuns = false;
|
|
139
|
+
private listeners = new Set<RunEventListener>();
|
|
140
|
+
private activeRunIds = new Set<string>();
|
|
141
|
+
private activeSessionIds = new Set<string>();
|
|
142
|
+
private readonly maxConcurrentRuns = 8;
|
|
143
|
+
|
|
144
|
+
constructor(private runtime: RuntimeEngine) {
|
|
145
|
+
ensureRunTables();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
start() {
|
|
149
|
+
this.ensureRecoveredState();
|
|
150
|
+
void this.kick();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stop() {
|
|
154
|
+
// no-op for now; run worker is tick-driven and has no timers
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
subscribe(onEvent: RunEventListener): () => void {
|
|
158
|
+
this.listeners.add(onEvent);
|
|
159
|
+
return () => {
|
|
160
|
+
this.listeners.delete(onEvent);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
createRun(input: CreateAgentRunInput): { run: AgentRun; deduplicated: boolean } {
|
|
165
|
+
ensureRunTables();
|
|
166
|
+
this.ensureRecoveredState();
|
|
167
|
+
|
|
168
|
+
const sessionId = input.sessionId.trim();
|
|
169
|
+
const content = input.content.trim();
|
|
170
|
+
const parts = normalizeRuntimeInputParts(input.parts);
|
|
171
|
+
const session = getSessionById(sessionId);
|
|
172
|
+
if (!session) {
|
|
173
|
+
throw new Error(`Unknown session: ${sessionId}`);
|
|
174
|
+
}
|
|
175
|
+
if (!content && parts.length === 0) {
|
|
176
|
+
throw new Error("content or parts is required");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const idempotencyKey = input.idempotencyKey?.trim() || null;
|
|
180
|
+
if (idempotencyKey) {
|
|
181
|
+
const existing = sqlite
|
|
182
|
+
.query(
|
|
183
|
+
`
|
|
184
|
+
SELECT *
|
|
185
|
+
FROM agent_runs
|
|
186
|
+
WHERE idempotency_key = ?1
|
|
187
|
+
LIMIT 1
|
|
188
|
+
`,
|
|
189
|
+
)
|
|
190
|
+
.get(idempotencyKey) as AgentRunRow | null;
|
|
191
|
+
if (existing) {
|
|
192
|
+
return {
|
|
193
|
+
run: rowToRun(existing),
|
|
194
|
+
deduplicated: true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const runId = createRunId();
|
|
200
|
+
const createdAt = nowMs();
|
|
201
|
+
const metadata = normalizeMetadata(input.metadata);
|
|
202
|
+
if (parts.length > 0) {
|
|
203
|
+
metadata[RUN_PARTS_METADATA_KEY] = parts;
|
|
204
|
+
}
|
|
205
|
+
const agent = input.agent?.trim();
|
|
206
|
+
if (agent) {
|
|
207
|
+
metadata.agent = agent;
|
|
208
|
+
}
|
|
209
|
+
const tx = sqlite.transaction(() => {
|
|
210
|
+
sqlite
|
|
211
|
+
.query(
|
|
212
|
+
`
|
|
213
|
+
INSERT INTO agent_runs (
|
|
214
|
+
id, session_id, state, content, metadata_json, idempotency_key,
|
|
215
|
+
result_json, error_json, created_at, updated_at, started_at, completed_at
|
|
216
|
+
)
|
|
217
|
+
VALUES (?1, ?2, 'queued', ?3, ?4, ?5, NULL, NULL, ?6, ?6, NULL, NULL)
|
|
218
|
+
`,
|
|
219
|
+
)
|
|
220
|
+
.run(runId, sessionId, content, safeJson(metadata), idempotencyKey, createdAt);
|
|
221
|
+
|
|
222
|
+
this.insertRunEvent(
|
|
223
|
+
runId,
|
|
224
|
+
"run.accepted",
|
|
225
|
+
{ sessionId, idempotencyKey, agent: agent ?? null, hasParts: parts.length > 0 },
|
|
226
|
+
createdAt,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
tx();
|
|
230
|
+
|
|
231
|
+
const run = this.getRunById(runId);
|
|
232
|
+
if (!run) {
|
|
233
|
+
throw new Error(`Failed to load created run: ${runId}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
void this.kick();
|
|
237
|
+
return { run, deduplicated: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getRunById(runId: string): AgentRun | null {
|
|
241
|
+
ensureRunTables();
|
|
242
|
+
const row = sqlite
|
|
243
|
+
.query(
|
|
244
|
+
`
|
|
245
|
+
SELECT *
|
|
246
|
+
FROM agent_runs
|
|
247
|
+
WHERE id = ?1
|
|
248
|
+
`,
|
|
249
|
+
)
|
|
250
|
+
.get(runId) as AgentRunRow | null;
|
|
251
|
+
return row ? rowToRun(row) : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
listRunEvents(input: {
|
|
255
|
+
runId: string;
|
|
256
|
+
afterSeq?: number;
|
|
257
|
+
limit?: number;
|
|
258
|
+
}): { events: AgentRunEvent[]; hasMore: boolean; nextAfterSeq: number } {
|
|
259
|
+
ensureRunTables();
|
|
260
|
+
const afterSeq = Math.max(0, Math.floor(input.afterSeq ?? 0));
|
|
261
|
+
const limit = Math.max(1, Math.min(500, Math.floor(input.limit ?? 100)));
|
|
262
|
+
const rows = sqlite
|
|
263
|
+
.query(
|
|
264
|
+
`
|
|
265
|
+
SELECT *
|
|
266
|
+
FROM agent_run_events
|
|
267
|
+
WHERE run_id = ?1
|
|
268
|
+
AND seq > ?2
|
|
269
|
+
ORDER BY seq ASC
|
|
270
|
+
LIMIT ?3
|
|
271
|
+
`,
|
|
272
|
+
)
|
|
273
|
+
.all(input.runId, afterSeq, limit + 1) as AgentRunEventRow[];
|
|
274
|
+
const hasMore = rows.length > limit;
|
|
275
|
+
const trimmed = hasMore ? rows.slice(0, limit) : rows;
|
|
276
|
+
const events = trimmed.map(rowToRunEvent);
|
|
277
|
+
const nextAfterSeq = events.length ? events[events.length - 1]!.seq : afterSeq;
|
|
278
|
+
return { events, hasMore, nextAfterSeq };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private ensureRecoveredState() {
|
|
282
|
+
if (this.recoveredPendingRuns) return;
|
|
283
|
+
this.recoveredPendingRuns = true;
|
|
284
|
+
|
|
285
|
+
const runningRows = sqlite
|
|
286
|
+
.query(
|
|
287
|
+
`
|
|
288
|
+
SELECT id
|
|
289
|
+
FROM agent_runs
|
|
290
|
+
WHERE state = 'running'
|
|
291
|
+
`,
|
|
292
|
+
)
|
|
293
|
+
.all() as Array<{ id: string }>;
|
|
294
|
+
if (!runningRows.length) return;
|
|
295
|
+
|
|
296
|
+
const recoveredAt = nowMs();
|
|
297
|
+
const tx = sqlite.transaction(() => {
|
|
298
|
+
sqlite
|
|
299
|
+
.query(
|
|
300
|
+
`
|
|
301
|
+
UPDATE agent_runs
|
|
302
|
+
SET state = 'queued', updated_at = ?1, started_at = NULL
|
|
303
|
+
WHERE state = 'running'
|
|
304
|
+
`,
|
|
305
|
+
)
|
|
306
|
+
.run(recoveredAt);
|
|
307
|
+
for (const row of runningRows) {
|
|
308
|
+
this.insertRunEvent(row.id, "run.recovered", { reason: "service restart" }, recoveredAt);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
tx();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async kick() {
|
|
315
|
+
if (this.dispatchInFlight) return;
|
|
316
|
+
this.dispatchInFlight = true;
|
|
317
|
+
try {
|
|
318
|
+
while (true) {
|
|
319
|
+
if (this.activeRunIds.size >= this.maxConcurrentRuns) break;
|
|
320
|
+
const claimed = this.claimNextQueuedRun(this.activeSessionIds);
|
|
321
|
+
if (!claimed) break;
|
|
322
|
+
this.activeRunIds.add(claimed.id);
|
|
323
|
+
this.activeSessionIds.add(claimed.session_id);
|
|
324
|
+
void this.executeRun(claimed)
|
|
325
|
+
.catch(() => {
|
|
326
|
+
// executeRun persists failures; dispatch loop only needs cleanup.
|
|
327
|
+
})
|
|
328
|
+
.finally(() => {
|
|
329
|
+
this.activeRunIds.delete(claimed.id);
|
|
330
|
+
this.activeSessionIds.delete(claimed.session_id);
|
|
331
|
+
void this.kick();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
this.dispatchInFlight = false;
|
|
336
|
+
if (this.hasRunnableQueuedRuns()) {
|
|
337
|
+
void this.kick();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private hasRunnableQueuedRuns() {
|
|
343
|
+
if (this.activeRunIds.size >= this.maxConcurrentRuns) return false;
|
|
344
|
+
if (this.activeSessionIds.size === 0) {
|
|
345
|
+
const row = sqlite
|
|
346
|
+
.query(
|
|
347
|
+
`
|
|
348
|
+
SELECT COUNT(*) as count
|
|
349
|
+
FROM agent_runs
|
|
350
|
+
WHERE state = 'queued'
|
|
351
|
+
`,
|
|
352
|
+
)
|
|
353
|
+
.get() as { count: number };
|
|
354
|
+
return row.count > 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const excludedSessionIds = [...this.activeSessionIds];
|
|
358
|
+
const placeholders = excludedSessionIds.map((_, index) => `?${index + 1}`).join(", ");
|
|
359
|
+
const row = sqlite
|
|
360
|
+
.query(
|
|
361
|
+
`
|
|
362
|
+
SELECT COUNT(*) as count
|
|
363
|
+
FROM agent_runs
|
|
364
|
+
WHERE state = 'queued'
|
|
365
|
+
AND session_id NOT IN (${placeholders})
|
|
366
|
+
`,
|
|
367
|
+
)
|
|
368
|
+
.get(...excludedSessionIds) as { count: number };
|
|
369
|
+
return row.count > 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private claimNextQueuedRun(excludedSessionIds: Set<string>): AgentRunRow | null {
|
|
373
|
+
ensureRunTables();
|
|
374
|
+
const excluded = [...excludedSessionIds];
|
|
375
|
+
const notInClause =
|
|
376
|
+
excluded.length > 0
|
|
377
|
+
? `AND session_id NOT IN (${excluded.map((_, index) => `?${index + 1}`).join(", ")})`
|
|
378
|
+
: "";
|
|
379
|
+
const tx = sqlite.transaction(() => {
|
|
380
|
+
const next = sqlite
|
|
381
|
+
.query(
|
|
382
|
+
`
|
|
383
|
+
SELECT *
|
|
384
|
+
FROM agent_runs
|
|
385
|
+
WHERE state = 'queued'
|
|
386
|
+
${notInClause}
|
|
387
|
+
ORDER BY created_at ASC
|
|
388
|
+
LIMIT 1
|
|
389
|
+
`,
|
|
390
|
+
)
|
|
391
|
+
.get(...excluded) as AgentRunRow | null;
|
|
392
|
+
if (!next) return null;
|
|
393
|
+
|
|
394
|
+
const claimedAt = nowMs();
|
|
395
|
+
sqlite
|
|
396
|
+
.query(
|
|
397
|
+
`
|
|
398
|
+
UPDATE agent_runs
|
|
399
|
+
SET state = 'running', started_at = COALESCE(started_at, ?2), updated_at = ?2
|
|
400
|
+
WHERE id = ?1
|
|
401
|
+
AND state = 'queued'
|
|
402
|
+
`,
|
|
403
|
+
)
|
|
404
|
+
.run(next.id, claimedAt);
|
|
405
|
+
const changed = sqlite.query("SELECT changes() as count").get() as { count: number };
|
|
406
|
+
if (changed.count < 1) return null;
|
|
407
|
+
|
|
408
|
+
const claimed = sqlite
|
|
409
|
+
.query(
|
|
410
|
+
`
|
|
411
|
+
SELECT *
|
|
412
|
+
FROM agent_runs
|
|
413
|
+
WHERE id = ?1
|
|
414
|
+
`,
|
|
415
|
+
)
|
|
416
|
+
.get(next.id) as AgentRunRow | null;
|
|
417
|
+
return claimed;
|
|
418
|
+
});
|
|
419
|
+
return tx();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async executeRun(run: AgentRunRow) {
|
|
423
|
+
const metadata = normalizeMetadata(parseJson(run.metadata_json));
|
|
424
|
+
const parts = normalizeRuntimeInputParts(metadata[RUN_PARTS_METADATA_KEY]);
|
|
425
|
+
delete metadata[RUN_PARTS_METADATA_KEY];
|
|
426
|
+
const agent = typeof metadata.agent === "string" ? metadata.agent.trim() : "";
|
|
427
|
+
const startedAt = nowMs();
|
|
428
|
+
this.insertRunEvent(run.id, "run.started", { sessionId: run.session_id, agent: agent || null }, startedAt);
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const ack = await this.runtime.sendUserMessage({
|
|
432
|
+
sessionId: run.session_id,
|
|
433
|
+
content: run.content,
|
|
434
|
+
parts,
|
|
435
|
+
agent: agent || undefined,
|
|
436
|
+
metadata,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const completedAt = nowMs();
|
|
440
|
+
const result = {
|
|
441
|
+
sessionId: ack.sessionId,
|
|
442
|
+
messageCount: ack.messages.length,
|
|
443
|
+
messageIds: ack.messages.map(message => message.id),
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const tx = sqlite.transaction(() => {
|
|
447
|
+
sqlite
|
|
448
|
+
.query(
|
|
449
|
+
`
|
|
450
|
+
UPDATE agent_runs
|
|
451
|
+
SET
|
|
452
|
+
state = 'completed',
|
|
453
|
+
updated_at = ?2,
|
|
454
|
+
completed_at = ?2,
|
|
455
|
+
result_json = ?3,
|
|
456
|
+
error_json = NULL
|
|
457
|
+
WHERE id = ?1
|
|
458
|
+
`,
|
|
459
|
+
)
|
|
460
|
+
.run(run.id, completedAt, safeJson(result));
|
|
461
|
+
this.insertRunEvent(run.id, "run.completed", result, completedAt);
|
|
462
|
+
});
|
|
463
|
+
tx();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error instanceof RuntimeContinuationDetachedError) {
|
|
466
|
+
const completedAt = nowMs();
|
|
467
|
+
const result = {
|
|
468
|
+
sessionId: run.session_id,
|
|
469
|
+
detached: true,
|
|
470
|
+
childRunCount: error.childRunCount,
|
|
471
|
+
};
|
|
472
|
+
const tx = sqlite.transaction(() => {
|
|
473
|
+
sqlite
|
|
474
|
+
.query(
|
|
475
|
+
`
|
|
476
|
+
UPDATE agent_runs
|
|
477
|
+
SET
|
|
478
|
+
state = 'completed',
|
|
479
|
+
updated_at = ?2,
|
|
480
|
+
completed_at = ?2,
|
|
481
|
+
result_json = ?3,
|
|
482
|
+
error_json = NULL
|
|
483
|
+
WHERE id = ?1
|
|
484
|
+
`,
|
|
485
|
+
)
|
|
486
|
+
.run(run.id, completedAt, safeJson(result));
|
|
487
|
+
this.insertRunEvent(run.id, "run.completed", result, completedAt);
|
|
488
|
+
});
|
|
489
|
+
tx();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (error instanceof RuntimeSessionQueuedError) {
|
|
494
|
+
const completedAt = nowMs();
|
|
495
|
+
const result = {
|
|
496
|
+
sessionId: run.session_id,
|
|
497
|
+
queued: true,
|
|
498
|
+
queueDepth: error.depth,
|
|
499
|
+
};
|
|
500
|
+
const tx = sqlite.transaction(() => {
|
|
501
|
+
sqlite
|
|
502
|
+
.query(
|
|
503
|
+
`
|
|
504
|
+
UPDATE agent_runs
|
|
505
|
+
SET
|
|
506
|
+
state = 'completed',
|
|
507
|
+
updated_at = ?2,
|
|
508
|
+
completed_at = ?2,
|
|
509
|
+
result_json = ?3,
|
|
510
|
+
error_json = NULL
|
|
511
|
+
WHERE id = ?1
|
|
512
|
+
`,
|
|
513
|
+
)
|
|
514
|
+
.run(run.id, completedAt, safeJson(result));
|
|
515
|
+
this.insertRunEvent(run.id, "run.completed", result, completedAt);
|
|
516
|
+
});
|
|
517
|
+
tx();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const failedAt = nowMs();
|
|
522
|
+
const details = {
|
|
523
|
+
name: error instanceof Error ? error.name : "Error",
|
|
524
|
+
message: error instanceof Error ? error.message : "Run execution failed",
|
|
525
|
+
};
|
|
526
|
+
const tx = sqlite.transaction(() => {
|
|
527
|
+
sqlite
|
|
528
|
+
.query(
|
|
529
|
+
`
|
|
530
|
+
UPDATE agent_runs
|
|
531
|
+
SET
|
|
532
|
+
state = 'failed',
|
|
533
|
+
updated_at = ?2,
|
|
534
|
+
completed_at = ?2,
|
|
535
|
+
error_json = ?3
|
|
536
|
+
WHERE id = ?1
|
|
537
|
+
`,
|
|
538
|
+
)
|
|
539
|
+
.run(run.id, failedAt, safeJson(details));
|
|
540
|
+
this.insertRunEvent(run.id, "run.failed", details, failedAt);
|
|
541
|
+
});
|
|
542
|
+
tx();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private insertRunEvent(runId: string, type: AgentRunEventType, payload: unknown, createdAt = nowMs()) {
|
|
547
|
+
const seq = nextRunEventSeq(runId);
|
|
548
|
+
const eventId = crypto.randomUUID();
|
|
549
|
+
sqlite
|
|
550
|
+
.query(
|
|
551
|
+
`
|
|
552
|
+
INSERT INTO agent_run_events (
|
|
553
|
+
id, run_id, seq, type, payload_json, created_at
|
|
554
|
+
)
|
|
555
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
|
556
|
+
`,
|
|
557
|
+
)
|
|
558
|
+
.run(eventId, runId, seq, type, safeJson(payload), createdAt);
|
|
559
|
+
|
|
560
|
+
const event: AgentRunEvent = {
|
|
561
|
+
id: eventId,
|
|
562
|
+
runId,
|
|
563
|
+
seq,
|
|
564
|
+
type,
|
|
565
|
+
payload,
|
|
566
|
+
at: toIso(createdAt),
|
|
567
|
+
};
|
|
568
|
+
for (const listener of this.listeners) {
|
|
569
|
+
try {
|
|
570
|
+
listener(event);
|
|
571
|
+
} catch {
|
|
572
|
+
// Best-effort only; listener failures should not affect run execution.
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { sqlite } from "../db/client";
|
|
2
|
+
|
|
3
|
+
export function ensureRunTables() {
|
|
4
|
+
sqlite.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS agent_runs (
|
|
6
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
7
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
8
|
+
state TEXT NOT NULL CHECK (state IN ('queued', 'running', 'completed', 'failed')),
|
|
9
|
+
content TEXT NOT NULL,
|
|
10
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
11
|
+
idempotency_key TEXT,
|
|
12
|
+
result_json TEXT,
|
|
13
|
+
error_json TEXT,
|
|
14
|
+
created_at INTEGER NOT NULL,
|
|
15
|
+
updated_at INTEGER NOT NULL,
|
|
16
|
+
started_at INTEGER,
|
|
17
|
+
completed_at INTEGER
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE UNIQUE INDEX IF NOT EXISTS agent_runs_idempotency_idx
|
|
21
|
+
ON agent_runs(idempotency_key)
|
|
22
|
+
WHERE idempotency_key IS NOT NULL;
|
|
23
|
+
CREATE INDEX IF NOT EXISTS agent_runs_state_created_idx
|
|
24
|
+
ON agent_runs(state, created_at ASC);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS agent_runs_session_created_idx
|
|
26
|
+
ON agent_runs(session_id, created_at DESC);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS agent_run_events (
|
|
29
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
30
|
+
run_id TEXT NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE,
|
|
31
|
+
seq INTEGER NOT NULL,
|
|
32
|
+
type TEXT NOT NULL,
|
|
33
|
+
payload_json TEXT NOT NULL,
|
|
34
|
+
created_at INTEGER NOT NULL,
|
|
35
|
+
UNIQUE(run_id, seq)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS agent_run_events_run_seq_idx
|
|
39
|
+
ON agent_run_events(run_id, seq ASC);
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function clearRunTables() {
|
|
44
|
+
ensureRunTables();
|
|
45
|
+
sqlite.query("DELETE FROM agent_run_events").run();
|
|
46
|
+
sqlite.query("DELETE FROM agent_runs").run();
|
|
47
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RuntimeInputPart } from "../contracts/runtime";
|
|
2
|
+
|
|
3
|
+
export type AgentRunState = "queued" | "running" | "completed" | "failed";
|
|
4
|
+
|
|
5
|
+
export type AgentRunEventType =
|
|
6
|
+
| "run.accepted"
|
|
7
|
+
| "run.recovered"
|
|
8
|
+
| "run.started"
|
|
9
|
+
| "run.completed"
|
|
10
|
+
| "run.failed";
|
|
11
|
+
|
|
12
|
+
export interface AgentRun {
|
|
13
|
+
id: string;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
state: AgentRunState;
|
|
16
|
+
content: string;
|
|
17
|
+
parts?: RuntimeInputPart[];
|
|
18
|
+
metadata: Record<string, unknown>;
|
|
19
|
+
idempotencyKey: string | null;
|
|
20
|
+
result: unknown;
|
|
21
|
+
error: unknown;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
startedAt: string | null;
|
|
25
|
+
completedAt: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AgentRunEvent {
|
|
29
|
+
id: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
seq: number;
|
|
32
|
+
type: AgentRunEventType;
|
|
33
|
+
payload: unknown;
|
|
34
|
+
at: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CreateAgentRunInput {
|
|
38
|
+
sessionId: string;
|
|
39
|
+
content: string;
|
|
40
|
+
parts?: RuntimeInputPart[];
|
|
41
|
+
agent?: string;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
43
|
+
idempotencyKey?: string;
|
|
44
|
+
}
|