You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Require meaningful slugs for spawned workers (#4621)
Require `spawn_worker` calls to include a meaningful slug, then append a
short random suffix so spawned worker URLs are readable and generally
unique. This makes child worker paths easier to identify in wake
messages, logs, and the UI.
## Root Cause
Workers previously used an opaque `nanoid(10)` id, so spawned worker
URLs were unique but not meaningful. When multiple workers were
dispatched, agents and humans had to infer purpose from surrounding
context instead of the worker path itself.
## Approach
- Add a required `slug` parameter to the `spawn_worker` tool schema.
- Normalize the slug into a safe path prefix:
- trim whitespace
- lowercase
- replace unsupported characters with `-`
- collapse repeated hyphens
- trim leading/trailing hyphens
- cap at 48 characters
- Append a short random hex suffix to avoid routine collisions:
```ts
const id = `${normalizedSlug}-${randomBytes(3).toString(`hex`)}`
```
- Reject slugs that normalize to no meaningful letter/number content
before spawning.
- Update Horton’s worker-spawning instructions to tell it to provide
short meaningful slugs.
- Remove stale fork-tool wording that said fork ids mirror the old
`spawn_worker` id parameter.
- Add a changeset for `@electric-ax/agents`.
## Key Invariants
- Every spawned worker id starts with a meaningful slug.
- Worker ids remain generally unique via the random suffix.
- Invalid slugs do not call `ctx.spawn`.
- Existing worker behavior remains otherwise unchanged:
- selected tools are forwarded
- model config is forwarded
- initial message is forwarded
- `runFinished` wake with response remains enabled
- sandbox inheritance remains enabled
## Non-goals
- This does not require globally collision-proof worker ids. If an
unlikely slug/suffix collision occurs, spawning fails and the agent can
retry.
- This does not restrict slugs to hyphen-only text; dots and underscores
remain allowed because worker ids become URLs and can be encoded.
- This does not change the fork id generation behavior beyond removing
stale documentation.
## Trade-offs
The implementation uses a small random suffix rather than
UUID/nanoid-level entropy to keep worker URLs compact. That preserves
readability while still avoiding collisions in normal use. Slugs are
capped at 48 characters to avoid unwieldy URLs, at the cost of
truncating very long model-provided labels.
## Verification
```bash
pnpm --filter @electric-ax/agents typecheck
pnpm --filter @electric-ax/agents exec vitest run test/spawn-worker-tool.test.ts
GITHUB_BASE_REF=main node scripts/check-changeset.mjs
```
## Files changed
- `.changeset/meaningful-worker-slugs.md`
- Adds a patch changeset for `@electric-ax/agents`.
- `packages/agents/src/tools/spawn-worker.ts`
- Requires `slug`.
- Normalizes and caps slug values.
- Appends a random hex suffix to form the worker id.
- Rejects non-meaningful slugs before spawning.
- `packages/agents/test/spawn-worker-tool.test.ts`
- Updates existing spawn-worker tests to pass slugs.
- Adds coverage for slug normalization, slug length capping, and invalid
slug rejection.
- `packages/agents/src/agents/horton.ts`
- Updates Horton’s instructions to provide meaningful slugs when
spawning workers.
- `packages/agents/src/tools/fork.ts`
- Removes stale wording tying fork ids to `spawn_worker` id behavior.
Copy file name to clipboardExpand all lines: packages/agents/src/agents/horton.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -333,7 +333,7 @@ Dispatch a worker when:
333
333
- The subtask is independent and can run in parallel with other work.
334
334
- You need an isolated context (e.g., focused coding on one file without pulling its full content into our chat).
335
335
336
-
When you spawn a worker, write its system prompt the way you'd brief a colleague who just walked in: include file paths, line numbers, what specifically to do, and what form of answer you want back. The system prompt sets the worker's persona and constraints; the required initialMessage is the concrete task you're handing off — that's what kicks the worker off, so without it the worker sits idle.
336
+
When you spawn a worker, provide a short meaningful slug for the worker path (for example, \`auth-audit\` or \`api-test-fix\`) and write its system prompt the way you'd brief a colleague who just walked in: include file paths, line numbers, what specifically to do, and what form of answer you want back. The system prompt sets the worker's persona and constraints; the required initialMessage is the concrete task you're handing off — that's what kicks the worker off, so without it the worker sits idle.
337
337
338
338
After spawning, end your turn (optionally with a brief "I've dispatched a worker for X; I'll respond when it finishes"). When the worker finishes, you'll receive a message describing which worker completed and what it returned. Multiple workers may finish at different times — check the message for the worker URL to know which one you're hearing about.
Copy file name to clipboardExpand all lines: packages/agents/src/tools/fork.ts
+2-3Lines changed: 2 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -23,7 +23,7 @@ Omit 'entityUrl' to fork your own session. Pass a different session's URL to for
23
23
),
24
24
id: Type.Optional(
25
25
Type.String({
26
-
description: `Instance id for the new fork (the \`<id>\` in \`/horton/<id>\`). Mirrors spawn_worker's id parameter. Omit to let the server assign one.`,
26
+
description: `Instance id for the new fork (the \`<id>\` in \`/horton/<id>\`). Omit to let the server assign one.`,
27
27
})
28
28
),
29
29
initialMessage: Type.Optional(
@@ -52,8 +52,7 @@ Omit 'entityUrl' to fork your own session. Pass a different session's URL to for
52
52
// The library API (`ctx.fork` / `ctx.forkSelf`) requires an id
53
53
// — same shape as `ctx.spawn(type, id, ...)`. The model layer
54
54
// doesn't need to know this; we generate one via nanoid when
55
-
// it's not supplied (same pattern `createSpawnWorkerTool` uses
description: `Dispatch a subagent (worker) to perform an isolated subtask. Provide a brief system prompt to give it its role and then a detailed initialMessage which briefs the worker like a colleague who just walked into the room (file paths, line numbers, what specifically to do, what form of answer you want back) and pick the subset of tools the worker needs.`,
43
+
description: `Dispatch a subagent (worker) to perform an isolated subtask. Provide a meaningful slug for the worker path, a brief system prompt to give it its role, then a detailed initialMessage which briefs the worker like a colleague who just walked into the room (file paths, line numbers, what specifically to do, what form of answer you want back), and pick the subset of tools the worker needs. The slug is normalized and a few random bytes are appended to keep the worker path unique.`,
29
44
parameters: Type.Object({
45
+
slug: Type.String({
46
+
description: `Short, meaningful slug for this worker, used as the start of its path. Use lowercase words separated by hyphens, e.g. "audit-auth-flow". A random suffix is added automatically for uniqueness.`,
47
+
}),
30
48
systemPrompt: Type.String({
31
49
description: `System prompt for the worker.`,
32
50
}),
@@ -41,11 +59,25 @@ export function createSpawnWorkerTool(
0 commit comments