Skip to content

Commit 07b09af

Browse files
feat: add agent factory runtime ledger
1 parent a746fb7 commit 07b09af

15 files changed

Lines changed: 1535 additions & 8 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
CREATE TABLE IF NOT EXISTS agent_factory_workflow_observations(
2+
id TEXT PRIMARY KEY,
3+
source_kind TEXT NOT NULL,
4+
source_id TEXT NOT NULL,
5+
external_id TEXT,
6+
observed_at TEXT NOT NULL,
7+
summary TEXT NOT NULL,
8+
actor TEXT,
9+
signals_json TEXT NOT NULL,
10+
payload_json TEXT,
11+
artifact_ids_json TEXT NOT NULL DEFAULT '[]',
12+
created_at TEXT NOT NULL
13+
);
14+
15+
CREATE TABLE IF NOT EXISTS agent_factory_agent_specs(
16+
id TEXT PRIMARY KEY,
17+
name TEXT NOT NULL,
18+
objective TEXT NOT NULL,
19+
status TEXT NOT NULL,
20+
version INTEGER NOT NULL,
21+
spec_json TEXT NOT NULL,
22+
created_by_agent_id TEXT,
23+
created_at TEXT NOT NULL,
24+
updated_at TEXT NOT NULL,
25+
FOREIGN KEY(created_by_agent_id) REFERENCES agents(id)
26+
);
27+
28+
CREATE TABLE IF NOT EXISTS agent_factory_candidate_workflows(
29+
id TEXT PRIMARY KEY,
30+
title TEXT NOT NULL,
31+
description TEXT NOT NULL,
32+
status TEXT NOT NULL,
33+
confidence REAL NOT NULL DEFAULT 0,
34+
risk TEXT NOT NULL,
35+
observation_ids_json TEXT NOT NULL,
36+
workflow_json TEXT NOT NULL,
37+
proposed_agent_spec_id TEXT,
38+
created_at TEXT NOT NULL,
39+
updated_at TEXT NOT NULL,
40+
FOREIGN KEY(proposed_agent_spec_id) REFERENCES agent_factory_agent_specs(id)
41+
);
42+
43+
CREATE TABLE IF NOT EXISTS agent_factory_deployment_plans(
44+
id TEXT PRIMARY KEY,
45+
agent_spec_id TEXT NOT NULL,
46+
candidate_workflow_id TEXT,
47+
status TEXT NOT NULL,
48+
plan_json TEXT NOT NULL,
49+
created_at TEXT NOT NULL,
50+
updated_at TEXT NOT NULL,
51+
FOREIGN KEY(agent_spec_id) REFERENCES agent_factory_agent_specs(id),
52+
FOREIGN KEY(candidate_workflow_id) REFERENCES agent_factory_candidate_workflows(id)
53+
);
54+
55+
CREATE TABLE IF NOT EXISTS agent_factory_approvals(
56+
id TEXT PRIMARY KEY,
57+
deployment_plan_id TEXT NOT NULL,
58+
approval_type TEXT NOT NULL,
59+
status TEXT NOT NULL,
60+
requested_by_agent_id TEXT,
61+
reviewed_by_agent_id TEXT,
62+
decision_json TEXT,
63+
requested_at TEXT NOT NULL,
64+
reviewed_at TEXT,
65+
FOREIGN KEY(deployment_plan_id) REFERENCES agent_factory_deployment_plans(id),
66+
FOREIGN KEY(requested_by_agent_id) REFERENCES agents(id),
67+
FOREIGN KEY(reviewed_by_agent_id) REFERENCES agents(id)
68+
);
69+
70+
CREATE TABLE IF NOT EXISTS agent_factory_evaluation_results(
71+
id TEXT PRIMARY KEY,
72+
agent_spec_id TEXT,
73+
deployment_plan_id TEXT,
74+
status TEXT NOT NULL,
75+
score REAL,
76+
result_json TEXT NOT NULL,
77+
artifact_id TEXT,
78+
created_at TEXT NOT NULL,
79+
FOREIGN KEY(agent_spec_id) REFERENCES agent_factory_agent_specs(id),
80+
FOREIGN KEY(deployment_plan_id) REFERENCES agent_factory_deployment_plans(id),
81+
FOREIGN KEY(artifact_id) REFERENCES artifacts(id)
82+
);
83+
84+
CREATE TABLE IF NOT EXISTS agent_factory_deployment_history(
85+
id INTEGER PRIMARY KEY AUTOINCREMENT,
86+
deployment_plan_id TEXT NOT NULL,
87+
event_type TEXT NOT NULL,
88+
payload_json TEXT,
89+
created_at TEXT NOT NULL,
90+
FOREIGN KEY(deployment_plan_id) REFERENCES agent_factory_deployment_plans(id)
91+
);
92+
93+
CREATE INDEX IF NOT EXISTS agent_factory_workflow_observations_source_idx ON agent_factory_workflow_observations(source_kind, source_id, observed_at);
94+
CREATE INDEX IF NOT EXISTS agent_factory_candidate_workflows_status_idx ON agent_factory_candidate_workflows(status, updated_at);
95+
CREATE INDEX IF NOT EXISTS agent_factory_agent_specs_status_idx ON agent_factory_agent_specs(status, updated_at);
96+
CREATE INDEX IF NOT EXISTS agent_factory_deployment_plans_spec_status_idx ON agent_factory_deployment_plans(agent_spec_id, status, updated_at);
97+
CREATE INDEX IF NOT EXISTS agent_factory_approvals_plan_status_idx ON agent_factory_approvals(deployment_plan_id, status);
98+
CREATE INDEX IF NOT EXISTS agent_factory_evaluation_results_plan_idx ON agent_factory_evaluation_results(deployment_plan_id, created_at);
99+
CREATE INDEX IF NOT EXISTS agent_factory_deployment_history_plan_idx ON agent_factory_deployment_history(deployment_plan_id, created_at);

crates/sparsekernel-core/src/lib.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ use std::process::{Command, Stdio};
1414
use std::time::Duration;
1515
use uuid::Uuid;
1616

17-
pub const SPARSEKERNEL_SCHEMA_VERSION: i64 = 4;
17+
pub const SPARSEKERNEL_SCHEMA_VERSION: i64 = 5;
1818
pub const SPARSEKERNEL_PROTOCOL_VERSION: &str = "2026-04-29.v1";
1919
const MIGRATION_0001: &str = include_str!("../migrations/0001_initial.sql");
2020
const MIGRATION_0002: &str = include_str!("../migrations/0002_browser_targets_observations.sql");
2121
const MIGRATION_0003: &str = include_str!("../migrations/0003_resource_lease_metadata.sql");
2222
const MIGRATION_0004: &str = include_str!("../migrations/0004_resource_budgets.sql");
23+
const MIGRATION_0005: &str = include_str!("../migrations/0005_agent_factory.sql");
2324
type SandboxLeaseReleaseRow = (
2425
Option<String>,
2526
Option<String>,
@@ -603,6 +604,13 @@ impl SparseKernelDb {
603604
params![4, now_iso()],
604605
)?;
605606
}
607+
if current < 5 {
608+
self.conn.execute_batch(MIGRATION_0005)?;
609+
self.conn.execute(
610+
"INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES(?, ?)",
611+
params![5, now_iso()],
612+
)?;
613+
}
606614
Ok(())
607615
})();
608616
match result {
@@ -657,6 +665,13 @@ impl SparseKernelDb {
657665
"capabilities",
658666
"audit_log",
659667
"usage_records",
668+
"agent_factory_workflow_observations",
669+
"agent_factory_candidate_workflows",
670+
"agent_factory_agent_specs",
671+
"agent_factory_deployment_plans",
672+
"agent_factory_approvals",
673+
"agent_factory_evaluation_results",
674+
"agent_factory_deployment_history",
660675
];
661676
let mut counts = BTreeMap::new();
662677
for table in tables {
@@ -3508,7 +3523,9 @@ mod tests {
35083523
assert_eq!(db.schema_version().unwrap(), SPARSEKERNEL_SCHEMA_VERSION);
35093524
db.migrate().unwrap();
35103525
assert_eq!(db.schema_version().unwrap(), SPARSEKERNEL_SCHEMA_VERSION);
3511-
assert_eq!(db.inspect().unwrap().counts["trust_zones"], 7);
3526+
let inspect = db.inspect().unwrap();
3527+
assert_eq!(inspect.counts["trust_zones"], 7);
3528+
assert_eq!(inspect.counts["agent_factory_agent_specs"], 0);
35123529
}
35133530

35143531
#[test]

docs/architecture/local-agent-kernel.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ openclaw runtime prune --older-than 7d
6464

6565
The DB uses WAL mode, foreign keys, a busy timeout, migrations, and typed runtime APIs. Agents and plugins should not open this database directly.
6666

67-
The initial schema includes agents, sessions, legacy session-entry mirrors, transcript events, messages, tasks, task events, tool calls, trust zones, network policies, resource leases, browser pools and contexts, browser targets, browser observations, artifacts, artifact access, capabilities, audit log, and usage records.
67+
The initial schema includes agents, sessions, legacy session-entry mirrors, transcript events, messages, tasks, task events, tool calls, trust zones, network policies, resource leases, browser pools and contexts, browser targets, browser observations, artifacts, artifact access, capabilities, audit log, usage records, and Agent Factory state for observed workflows, generated specs, approvals, evaluation results, and deployment history.
6868

6969
## Artifact store
7070

docs/architecture/sparsekernel.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Small machines can keep many logical agents parked in durable state, but they ca
2727
- Artifact Store: content-addressed files for large blobs.
2828
- Task and Lease Scheduler: bounded active execution.
2929
- Capability Engine: allow, deny, revoke, and audit sensitive actions.
30+
- Agent Factory: discovery, spec generation, and policy compilation for agents that should exist.
3031
- Tool Broker: mediated tool invocation.
3132
- Browser Broker: process pools by trust zone and contexts by task/session.
3233
- Sandbox Broker: backend abstraction for local, bwrap, Docker, minijail, smolvm, or VM backends.
@@ -37,11 +38,21 @@ Small machines can keep many logical agents parked in durable state, but they ca
3738
- `crates/sparsekernel-core`: Rust ledger, artifacts, task leases, capabilities, audit, and mock brokers.
3839
- `crates/sparsekernel-cli`: `sparsekernel` CLI and `sparsekerneld` local daemon.
3940
- `crates/sparsekernel-core/migrations/0001_initial.sql`: initial SQLite schema.
41+
- `packages/agent-factory`: pure TypeScript contracts and helpers for workflow observations, candidate workflows, agent specs, policy compilation, and deployment plans.
42+
- `packages/agent-factory-connectors`: connector adapter contracts for Gmail, CRM, calendar, accounting, and ticketing discovery surfaces.
4043
- `packages/browser-broker`: TypeScript CDP adapter that materializes leased browser contexts and artifactizes screenshots/downloads.
4144
- `packages/openclaw-sparsekernel-adapter`: TypeScript adapter that wraps OpenClaw-shaped tool execution with daemon-backed session upsert, capability grants, tool-call lifecycle transitions, and oversized-output artifactization.
4245
- `packages/sparsekernel-client`: small TypeScript daemon client.
4346
- `schemas/`: API and event schema definitions.
4447

48+
## Agent Factory Boundary
49+
50+
Agent Factory decides what should exist. SparseKernel decides how it safely runs.
51+
52+
The factory package turns observed workflows into candidate workflows, agent specs, compiled policy, and deployment plans. It does not allocate browser contexts, open sandboxes, invoke connector tools, or grant capabilities directly. Deployment plans contain kernel requests that the SparseKernel runtime can approve, audit, and materialize through broker APIs.
53+
54+
Agent Factory runtime history uses first-class ledger tables for observed workflows, generated specs, approvals, evaluation results, and deployment history. Large supporting evidence such as screenshots, exports, traces, PDFs, and transcripts remains in the artifact store with SQLite metadata and access records.
55+
4556
## Local API
4657

4758
The v0 daemon exposes localhost JSON endpoints for health/status, session upsert/list, transcript event append/list, task enqueue/claim-by-id/claim-next/heartbeat/complete/fail, expired lease release, tool-call create/start/complete/fail/list, artifact create/read/metadata, browser context acquire/release/list, loopback CDP endpoint probing, sandbox allocate/release, capability grant/check/list/revoke, task listing, and audit listing. `/health` advertises `protocol_version`, `schema_version`, and feature strings so clients can reject incompatible daemon protocols before making state-changing calls. The TypeScript client uses those endpoints instead of opening the SQLite file directly.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@openclaw/agent-factory-connectors",
3+
"version": "0.0.0-private",
4+
"private": true,
5+
"type": "module",
6+
"exports": {
7+
".": "./src/index.ts"
8+
}
9+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
createAgentFactoryConnectorRegistry,
4+
createStaticConnectorAdapter,
5+
workflowObservationFromConnectorEvent,
6+
} from "./index.js";
7+
8+
describe("@openclaw/agent-factory-connectors", () => {
9+
it("registers connectors by id and lists them deterministically", () => {
10+
const gmail = createStaticConnectorAdapter({
11+
id: "gmail-primary",
12+
kind: "gmail",
13+
displayName: "Gmail",
14+
});
15+
const crm = createStaticConnectorAdapter({
16+
id: "crm-primary",
17+
kind: "crm",
18+
displayName: "CRM",
19+
});
20+
const registry = createAgentFactoryConnectorRegistry([gmail, crm]);
21+
22+
expect(registry.get("gmail-primary")).toBe(gmail);
23+
expect(registry.list().map((adapter) => adapter.id)).toEqual(["crm-primary", "gmail-primary"]);
24+
expect(registry.listByKind("gmail")).toEqual([gmail]);
25+
expect(() => registry.register(gmail)).toThrow(/already registered/);
26+
});
27+
28+
it("adapts connector events into workflow observations", () => {
29+
expect(
30+
workflowObservationFromConnectorEvent({
31+
id: "thread-1",
32+
connectorId: "gmail-primary",
33+
connectorKind: "gmail",
34+
occurredAt: "2026-05-07T12:00:00.000Z",
35+
summary: "Customer asks for appointment availability.",
36+
actor: "customer@example.com",
37+
signals: [{ kind: "repetition", label: "common support request", confidence: 0.9 }],
38+
payload: { subject: "Availability" },
39+
}),
40+
).toEqual({
41+
id: "thread-1",
42+
source: {
43+
connectorKind: "gmail",
44+
connectorId: "gmail-primary",
45+
externalId: "thread-1",
46+
},
47+
observedAt: "2026-05-07T12:00:00.000Z",
48+
summary: "Customer asks for appointment availability.",
49+
actor: "customer@example.com",
50+
signals: [{ kind: "repetition", label: "common support request", confidence: 0.9 }],
51+
payload: { subject: "Availability" },
52+
});
53+
});
54+
55+
it("supports static connector observations for deterministic tests and demos", async () => {
56+
const adapter = createStaticConnectorAdapter({
57+
id: "tickets-primary",
58+
kind: "ticketing",
59+
displayName: "Ticketing",
60+
events: [
61+
{
62+
id: "ticket-old",
63+
connectorId: "tickets-primary",
64+
connectorKind: "ticketing",
65+
occurredAt: "2026-05-06T12:00:00.000Z",
66+
summary: "Old ticket",
67+
},
68+
{
69+
id: "ticket-new",
70+
connectorId: "tickets-primary",
71+
connectorKind: "ticketing",
72+
occurredAt: "2026-05-07T12:00:00.000Z",
73+
summary: "New ticket",
74+
},
75+
],
76+
});
77+
78+
await expect(adapter.observe({ since: "2026-05-07T00:00:00.000Z", limit: 1 })).resolves.toEqual(
79+
[
80+
expect.objectContaining({
81+
id: "ticket-new",
82+
}),
83+
],
84+
);
85+
});
86+
});

0 commit comments

Comments
 (0)