Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/deep-survey-worker-shared-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@electric-ax/agents-runtime': patch
'@electric-ax/agents': patch
---

Add shared state support to worker agents and deep survey example

- Worker agents can now observe a shared state DB via `sharedDb` spawn arg, generating per-collection CRUD tools
- New `sharedDbToolMode` option controls whether `full` (read/write/update/delete) or `write-only` tools are generated
- Rename `schema` parameter to `dbSchema` in `db()` observation source to avoid shadowing
5 changes: 5 additions & 0 deletions .changeset/fix-postgres-18-volume-mount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'electric-ax': patch
---

Fix postgres 18 docker volume mount path to use `/var/lib/postgresql` instead of `/var/lib/postgresql/data`
35 changes: 35 additions & 0 deletions examples/deep-survey/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@electric-ax/example-deep-survey",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "pnpm run --parallel \"/^dev:/\"",
"dev:server": "tsx watch src/server/index.ts",
"dev:ui": "vite",
"build:ui": "vite build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@electric-ax/agents-runtime": "workspace:*",
"@durable-streams/state": "npm:@electric-ax/durable-streams-state-beta@^0.3.0",
"@radix-ui/themes": "^3.3.0",
"@tanstack/db": "^0.6.0",
"@tanstack/react-db": "^0.1.78",
"@mariozechner/pi-agent-core": "^0.57.1",
"@sinclair/typebox": "^0.34.0",
"d3-force": "^3.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/d3-force": "^3.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0",
"tsx": "^4.0.0",
"typescript": "~5.9.3",
"vite": "^7.2.4"
}
}
48 changes: 48 additions & 0 deletions examples/deep-survey/src/server/explorer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { SharedStateSchemaMap } from '@electric-ax/agents-runtime'

export const EXPLORER_SYSTEM_PROMPT = (
topic: string,
corpus: string,
childId: string
) =>
`You are an explorer agent in a deep survey analyzing: "${corpus}".

Your assigned topic is: "${topic}"
Your entity ID is: "${childId}"

You have web search and URL fetching tools to do real research, plus shared-state tools to write your findings.

Your job:
1. Use web_search to research your topic (2-3 searches). Use fetch_url to read the most relevant pages.

2. Write a concise wiki entry (100-200 words) synthesizing what you learned using the write_wiki tool.
- Set "key" to exactly "${childId}" (your entity ID — this is critical for cross-linking)
- Set "title" to a descriptive title
- Set "body" to your research findings (include specific facts, not vague summaries)
- Set "author" to "${topic} Explorer"
- Set "improved" to false

3. After writing your entry, use read_wiki to scan other entries in the shared wiki.
For each entry that is meaningfully related to yours, use write_xrefs to record the connection:
- Set "key" to a deterministic edge id like "your-key--other-key" (alphabetical order)
- Set "a" to your entry's key ("${childId}")
- Set "b" to the other entry's key (their key field)

4. After writing your entry and any cross-references, stop.

Be specific and insightful. Focus on real facts from your research.`

export function explorerSpawnArgs(
topic: string,
corpus: string,
sharedStateId: string,
sharedSchema: SharedStateSchemaMap,
childId: string
) {
return {
systemPrompt: EXPLORER_SYSTEM_PROMPT(topic, corpus, childId),
tools: [`brave_search`, `fetch_url`],
sharedDb: { id: sharedStateId, schema: sharedSchema },
sharedDbToolMode: `full` as const,
}
}
123 changes: 123 additions & 0 deletions examples/deep-survey/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import path from 'node:path'

try {
process.loadEnvFile(path.resolve(import.meta.dirname, `../../../../.env`))
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== `ENOENT`) {
console.error(`Failed to load .env file:`, err)
}
}

import {
createEntityRegistry,
createRuntimeHandler,
} from '@electric-ax/agents-runtime'
import http from 'node:http'
import { registerOrchestrator } from './orchestrator.js'

const DARIX_URL = process.env.DARIX_URL ?? `http://localhost:4437`
const PORT = Number(process.env.PORT ?? 4700)
const SERVE_URL = process.env.SERVE_URL ?? `http://localhost:${PORT}`

const registry = createEntityRegistry()
registerOrchestrator(registry)

const runtime = createRuntimeHandler({
baseUrl: DARIX_URL,
serveEndpoint: `${SERVE_URL}/webhook`,
registry,
})

function writeJson(
res: http.ServerResponse,
status: number,
body: unknown
): void {
res.writeHead(status, {
'Content-Type': `application/json`,
'Access-Control-Allow-Origin': `*`,
})
res.end(JSON.stringify(body))
}

async function readJson(req: http.IncomingMessage): Promise<unknown> {
const chunks: Array<Buffer> = []
for await (const chunk of req) chunks.push(chunk as Buffer)
return JSON.parse(Buffer.concat(chunks).toString(`utf8`))
}

const server = http.createServer(async (req, res) => {
if (req.method === `OPTIONS`) {
res.writeHead(204, {
'Access-Control-Allow-Origin': `*`,
'Access-Control-Allow-Methods': `POST, GET, OPTIONS`,
'Access-Control-Allow-Headers': `Content-Type`,
})
res.end()
return
}

if (req.url === `/webhook` && req.method === `POST`) {
await runtime.onEnter(req, res)
return
}

if (req.url === `/api/swarm` && req.method === `POST`) {
try {
const body = (await readJson(req)) as {
name?: string
corpus?: string
message?: string
}

const name = body.name ?? crypto.randomUUID().slice(0, 8)
const message =
body.message ??
(body.corpus
? `Explore this corpus: ${body.corpus}`
: `Explore the React source code — map the reconciler, hooks, scheduler, and all major subsystems.`)

const putRes = await fetch(`${DARIX_URL}/orchestrator/${name}`, {
method: `PUT`,
headers: { 'Content-Type': `application/json` },
body: JSON.stringify({
args: {},
tags: { swarm_id: name },
initialMessage: message,
}),
})

if (!putRes.ok) {
const text = await putRes.text()
writeJson(res, 500, { error: `spawn failed: ${text}` })
return
}

writeJson(res, 200, {
name,
orchestratorUrl: `/orchestrator/${name}`,
swarmId: name,
})
} catch (err) {
writeJson(res, 500, {
error: err instanceof Error ? err.message : String(err),
})
}
return
}

if (req.url === `/api/config` && req.method === `GET`) {
writeJson(res, 200, { darixUrl: DARIX_URL })
return
}

res.writeHead(404)
res.end()
})

server.listen(PORT, async () => {
await runtime.registerTypes()
console.log(`Deep Survey server ready on port ${PORT}`)
console.log(`DARIX: ${DARIX_URL}`)
console.log(`${runtime.typeNames.length} entity types registered`)
})
Loading
Loading