lampson 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/.env.example +29 -0
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/lampson.js +81 -0
- package/chat.syn +799 -0
- package/lamps/example-hello/lamp.json +16 -0
- package/lamps/example-hello/lamp.syn +19 -0
- package/lampson.cmd +4 -0
- package/lampson.ps1 +88 -0
- package/lampson.sh +42 -0
- package/lib/agents.syn +471 -0
- package/lib/git.syn +58 -0
- package/lib/lamps.syn +386 -0
- package/lib/loop.syn +455 -0
- package/lib/lsp.syn +503 -0
- package/lib/mcp.syn +403 -0
- package/lib/permission.syn +154 -0
- package/lib/prompt.syn +75 -0
- package/lib/provider.syn +522 -0
- package/lib/session.syn +111 -0
- package/lib/settings.syn +70 -0
- package/lib/skills.syn +179 -0
- package/lib/tools/bash.syn +105 -0
- package/lib/tools/common.sh +49 -0
- package/lib/tools/common.syn +91 -0
- package/lib/tools/edit.syn +32 -0
- package/lib/tools/find.syn +35 -0
- package/lib/tools/grep.syn +31 -0
- package/lib/tools/img.ps1 +36 -0
- package/lib/tools/img.sh +22 -0
- package/lib/tools/ls.syn +18 -0
- package/lib/tools/memo.syn +148 -0
- package/lib/tools/proc.sh +42 -0
- package/lib/tools/proc.syn +314 -0
- package/lib/tools/process.syn +46 -0
- package/lib/tools/read.syn +25 -0
- package/lib/tools/skill.syn +14 -0
- package/lib/tools/todo.syn +97 -0
- package/lib/tools/write.syn +22 -0
- package/lib/tools.syn +198 -0
- package/lib/trace.syn +116 -0
- package/lib/tree.syn +59 -0
- package/lib/update.syn +57 -0
- package/package.json +40 -0
- package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-400-latin.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin.woff2 +0 -0
- package/public/index.html +1268 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +218 -0
- package/public/vendor/xterm.js +2 -0
- package/skills/debugging/SKILL.md +33 -0
- package/skills/lampson/SKILL.md +117 -0
- package/skills/synsema/SKILL.md +75 -0
- package/web.syn +468 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-hello",
|
|
3
|
+
"description": "Example lamp: a Synsema program run under a capability ceiling. Copy this folder to make your own.",
|
|
4
|
+
"kind": "syn",
|
|
5
|
+
"entry": "lamp.syn",
|
|
6
|
+
"caps": "",
|
|
7
|
+
"timeout": 20,
|
|
8
|
+
"tools": [
|
|
9
|
+
{
|
|
10
|
+
"name": "greet",
|
|
11
|
+
"description": "Greets someone (demo). Returns a short text.",
|
|
12
|
+
"parameters": {"type": "object", "properties": {"who": {"type": "string", "description": "name to greet"}}, "required": ["who"]},
|
|
13
|
+
"readonly": true
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- lamps/example-hello/lamp.syn — la lámpara de ejemplo
|
|
2
|
+
--
|
|
3
|
+
-- Lampson la corre así por cada llamada: synsema run --cap-set stdout,time,env=LAMP_* lamp.syn
|
|
4
|
+
-- El techo (--cap-set) sale del manifiesto (lamp.json → "caps") que el humano aprobó al encenderla;
|
|
5
|
+
-- pedir más acá (p. ej. `require net`) falla con "above the host ceiling".
|
|
6
|
+
-- Entrada por env: LAMP_TOOL (qué tool), LAMP_ARGS (sus args en JSON), LAMP_DIR, LAMP_WORKSPACE.
|
|
7
|
+
-- Salida: lo que imprimas por stdout vuelve al modelo como resultado de la tool.
|
|
8
|
+
intent: "example lamp for lampson: greet"
|
|
9
|
+
|
|
10
|
+
require env("LAMP_*")
|
|
11
|
+
|
|
12
|
+
let tool be env("LAMP_TOOL", "")
|
|
13
|
+
let args be json_decode(env("LAMP_ARGS", "{}"))
|
|
14
|
+
|
|
15
|
+
when tool == "greet"
|
|
16
|
+
let who be when contains(args, "who") then text(args["who"]) otherwise "world"
|
|
17
|
+
print("hello, " + who + "! (from the example-hello lamp, running under a capability ceiling)")
|
|
18
|
+
otherwise
|
|
19
|
+
print("ERROR: unknown tool '" + tool + "'")
|
package/lampson.cmd
ADDED
package/lampson.ps1
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# lampson.ps1 — launcher Windows. Monta el proyecto como ./workspace (junction) y arranca.
|
|
2
|
+
#
|
|
3
|
+
# cd C:\mi\proyecto ; lampson # el directorio ACTUAL es el workspace (REPL)
|
|
4
|
+
# cd C:\mi\proyecto ; lampson --web # servidor web en http://127.0.0.1:8080 (también -Web)
|
|
5
|
+
# lampson --workspace C:\otro\proyecto # elegir la ubicación explícitamente (también -Workspace)
|
|
6
|
+
# lampson --agent plan # perfil inicial: build | plan | review | explore
|
|
7
|
+
# lampson --yolo | --strict | --ask # permisos para comandos peligrosos (--dangerously-skip-permissions = --yolo)
|
|
8
|
+
# lampson --update # actualizar Lampson (git pull) y salir
|
|
9
|
+
# lampson --help
|
|
10
|
+
#
|
|
11
|
+
# Por qué una junction: el scope file("./*") de Synsema v0.6.7 equivale a "*" (disco entero); el scope
|
|
12
|
+
# file("workspace/*") sí confina. Montar el proyecto bajo un nombre literal es lo que hace real el
|
|
13
|
+
# least-privilege de las tools (mismo modelo mental que `docker -v proyecto:/workspace`).
|
|
14
|
+
$ErrorActionPreference = "Stop"
|
|
15
|
+
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
16
|
+
$caller = (Get-Location).Path
|
|
17
|
+
$mount = Join-Path $here "workspace"
|
|
18
|
+
|
|
19
|
+
# --- args: acepta --flag y -Flag, sin distinguir mayúsculas ---
|
|
20
|
+
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""
|
|
21
|
+
$i = 0
|
|
22
|
+
while ($i -lt $args.Count) {
|
|
23
|
+
$a = [string]$args[$i]
|
|
24
|
+
switch -Regex ($a.ToLower()) {
|
|
25
|
+
'^--?(web|w)$' { $Web = $true }
|
|
26
|
+
'^--?(workspace|ws)$' { $i++; $Workspace = [string]$args[$i] }
|
|
27
|
+
'^--?(agent|a)$' { $i++; $Agent = [string]$args[$i] }
|
|
28
|
+
'^--?(yolo|y|dangerously-skip-permissions)$' { $Perm = "yolo" }
|
|
29
|
+
'^--?(strict|s)$' { $Perm = "strict" }
|
|
30
|
+
'^--?(ask)$' { $Perm = "ask" }
|
|
31
|
+
'^--?(permission|p)$' { $i++; $Perm = ([string]$args[$i]).ToLower() }
|
|
32
|
+
'^--?(update|u)$' { Write-Host "actualizando Lampson en $here"; git -C $here pull --ff-only origin main; Write-Host ("lampson " + (git -C $here rev-parse --short HEAD)); exit $LASTEXITCODE }
|
|
33
|
+
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First 9 | ForEach-Object { $_.TrimStart('#',' ') }; exit 0 }
|
|
34
|
+
default { if ($Workspace -eq "" -and -not $a.StartsWith("-")) { $Workspace = $a } else { Write-Error "argumento desconocido: $a (probá lampson --help)"; exit 1 } }
|
|
35
|
+
}
|
|
36
|
+
$i++
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# --- resolver el proyecto: explícito > directorio actual (si no es el propio lampson) > montado previo ---
|
|
40
|
+
if ($Workspace -eq "" -and $caller -ne $here) { $Workspace = $caller }
|
|
41
|
+
if ($Workspace -ne "") {
|
|
42
|
+
if (-not (Test-Path -LiteralPath $Workspace -PathType Container)) { Write-Error "el workspace no existe o no es un directorio: $Workspace"; exit 1 }
|
|
43
|
+
$target = (Resolve-Path -LiteralPath $Workspace).Path
|
|
44
|
+
$home_ = [Environment]::GetFolderPath("UserProfile")
|
|
45
|
+
if ($target.TrimEnd('\') -eq $home_.TrimEnd('\') -or $target -match '^[A-Za-z]:\\?$') {
|
|
46
|
+
Write-Error "'$target' es tu carpeta personal / la raíz del disco, no un proyecto. Entrá al repo (cd) y volvé a correr lampson, o usá --workspace C:\ruta\al\proyecto"; exit 1
|
|
47
|
+
}
|
|
48
|
+
if ($target -eq $here) { Write-Error "no montes el propio directorio de lampson como workspace desde fuera; usá --workspace"; exit 1 }
|
|
49
|
+
if (Test-Path -LiteralPath $mount) {
|
|
50
|
+
$item = Get-Item -LiteralPath $mount -Force
|
|
51
|
+
if ($item.LinkType -ne "Junction") { Write-Error "./workspace existe y no es una junction; movelo antes de montar otro proyecto"; exit 1 }
|
|
52
|
+
$item.Delete()
|
|
53
|
+
}
|
|
54
|
+
New-Item -ItemType Junction -Path $mount -Target $target | Out-Null
|
|
55
|
+
} elseif (-not (Test-Path -LiteralPath $mount)) {
|
|
56
|
+
Write-Error "no hay workspace: corré lampson desde el directorio del proyecto, o lampson --workspace C:\ruta"; exit 1
|
|
57
|
+
}
|
|
58
|
+
$target = (Get-Item -LiteralPath $mount -Force).Target
|
|
59
|
+
if ($target -is [array]) { $target = $target[0] }
|
|
60
|
+
|
|
61
|
+
# --- skills externas globales (npx skills add -g → ~/.agents/skills; las de Claude Code → ~/.claude/skills) ---
|
|
62
|
+
# Se montan como junction bajo .lampson/ porque una capability no puede apuntar a una ruta dinámica (HOME).
|
|
63
|
+
$skillMounts = @{ "skills-global" = (Join-Path $HOME ".agents\skills"); "skills-claude" = (Join-Path $HOME ".claude\skills") }
|
|
64
|
+
New-Item -ItemType Directory -Force (Join-Path $here ".lampson") | Out-Null
|
|
65
|
+
foreach ($k in $skillMounts.Keys) {
|
|
66
|
+
$link = Join-Path $here ".lampson\$k"; $src = $skillMounts[$k]
|
|
67
|
+
if (Test-Path -LiteralPath $link) { $li = Get-Item -LiteralPath $link -Force; if ($li.LinkType -eq "Junction") { $li.Delete() } }
|
|
68
|
+
if (Test-Path -LiteralPath $src -PathType Container) { New-Item -ItemType Junction -Path $link -Target $src | Out-Null }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# --- arrancar desde el directorio de lampson (ahí viven .env, lib/, skills/, .lampson/) ---
|
|
72
|
+
# Push/Pop: si PowerShell ejecuta este .ps1 en la shell del usuario (pasa cuando lampson.ps1 y lampson.cmd
|
|
73
|
+
# comparten nombre en el PATH), el cwd del usuario debe quedar como estaba al salir.
|
|
74
|
+
Push-Location $here
|
|
75
|
+
try {
|
|
76
|
+
$env:LAMPSON_WORKSPACE = $target
|
|
77
|
+
if ($Agent -ne "") { $env:LAMPSON_AGENT = $Agent }
|
|
78
|
+
if ($Perm -ne "") { $env:LAMPSON_PERMISSION = $Perm }
|
|
79
|
+
if ($Web) {
|
|
80
|
+
Write-Host "Lampson web · workspace: $target"
|
|
81
|
+
Write-Host "abrí http://127.0.0.1:8080 (Ctrl+C para parar)"
|
|
82
|
+
synsema serve web.syn
|
|
83
|
+
} else {
|
|
84
|
+
synsema run chat.syn
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
Pop-Location
|
|
88
|
+
}
|
package/lampson.sh
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lampson.sh — launcher unix. El directorio ACTUAL es el workspace (o --workspace /ruta).
|
|
3
|
+
# cd /mi/proyecto && lampson # REPL (con lampson/ en el PATH)
|
|
4
|
+
# cd /mi/proyecto && lampson --web # servidor web en http://127.0.0.1:8080
|
|
5
|
+
# lampson --workspace /otro/proyecto [--web] [--agent plan]
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
caller="$(pwd)"
|
|
9
|
+
ws=""; web=0
|
|
10
|
+
while [ $# -gt 0 ]; do
|
|
11
|
+
case "$1" in
|
|
12
|
+
--web) web=1 ;;
|
|
13
|
+
--workspace) shift; ws="$1" ;;
|
|
14
|
+
--agent) shift; export LAMPSON_AGENT="$1" ;;
|
|
15
|
+
--yolo|--dangerously-skip-permissions) export LAMPSON_PERMISSION=yolo ;;
|
|
16
|
+
--strict) export LAMPSON_PERMISSION=strict ;;
|
|
17
|
+
--ask) export LAMPSON_PERMISSION=ask ;;
|
|
18
|
+
--update) echo "actualizando Lampson en $here"; git -C "$here" pull --ff-only origin main; echo "lampson $(git -C "$here" rev-parse --short HEAD)"; exit $? ;;
|
|
19
|
+
*) echo "uso: lampson [--web] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
|
|
20
|
+
esac
|
|
21
|
+
shift
|
|
22
|
+
done
|
|
23
|
+
[ -z "$ws" ] && [ "$caller" != "$here" ] && ws="$caller"
|
|
24
|
+
if [ -n "$ws" ]; then
|
|
25
|
+
[ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
|
|
26
|
+
ws="$(cd "$ws" && pwd)"
|
|
27
|
+
if [ -e "$here/workspace" ] && [ ! -L "$here/workspace" ]; then echo "./workspace existe y no es un symlink" >&2; exit 1; fi
|
|
28
|
+
rm -f "$here/workspace"; ln -s "$ws" "$here/workspace"
|
|
29
|
+
elif [ ! -e "$here/workspace" ]; then
|
|
30
|
+
echo "no hay workspace: corré lampson desde el directorio del proyecto, o --workspace /ruta" >&2; exit 1
|
|
31
|
+
fi
|
|
32
|
+
export LAMPSON_WORKSPACE="$(readlink -f "$here/workspace")"
|
|
33
|
+
# skills externas globales (npx skills add -g → ~/.agents/skills; Claude Code → ~/.claude/skills), montadas bajo .lampson/
|
|
34
|
+
mkdir -p "$here/.lampson"
|
|
35
|
+
for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/skills"; do
|
|
36
|
+
link="$here/.lampson/${pair%%:*}"; src="${pair#*:}"
|
|
37
|
+
[ -L "$link" ] && rm -f "$link"
|
|
38
|
+
[ -d "$src" ] && ln -s "$src" "$link"
|
|
39
|
+
done
|
|
40
|
+
echo "workspace -> $LAMPSON_WORKSPACE"
|
|
41
|
+
cd "$here"
|
|
42
|
+
if [ "$web" = 1 ]; then echo "web: http://127.0.0.1:8080"; exec synsema serve web.syn; else exec synsema run chat.syn; fi
|
package/lib/agents.syn
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
-- lib/agents.syn — perfiles de agente + la tool `delegate` (subagentes)
|
|
2
|
+
--
|
|
3
|
+
-- Un "modo" (plan, review…) NO es un estado del runtime: es un agente con un allow-list de tools
|
|
4
|
+
-- restringido y un addendum al system prompt. Cambiar de modo = cambiar de perfil.
|
|
5
|
+
--
|
|
6
|
+
-- SUBAGENTES (2026-08-27, tomado de hermes/deepseek/opencode — ver notes/*.md):
|
|
7
|
+
-- * `delegate(tasks=[{agent, brief, context}…])` corre N hijos EN PARALELO (parallel_map: hilos reales),
|
|
8
|
+
-- cada uno con historial propio, perfil restringido y sin `delegate` (profundidad 1). Devuelve un
|
|
9
|
+
-- informe consolidado: por hijo {status, steps, tokens, texto}. El texto es un SELF-REPORT: el padre
|
|
10
|
+
-- debe verificar (rutas, tests) antes de darlo por bueno.
|
|
11
|
+
-- * `background=true`: vuelve enseguida con el id; el hijo corre en un `agent` de Synsema (hilo con
|
|
12
|
+
-- intérprete propio) y, al terminar, el resultado entra en el buzón del padre (loop.inbox_fn) como
|
|
13
|
+
-- mensaje nuevo en su próximo paso — o, si el padre está idle en la terminal, como turno nuevo.
|
|
14
|
+
-- Nunca se muta contexto pasado (prefix cache intacto).
|
|
15
|
+
-- * Cada hijo deja un log vivo en .lampson/agents/<id>.log (tail -f / panel web) y su resultado en
|
|
16
|
+
-- <id>.json. `delegate(action=list|steer|stop|result, id, message)`: steer = texto que el hijo ve en
|
|
17
|
+
-- su próximo paso; stop = corta y devuelve lo parcial.
|
|
18
|
+
-- * Permisos del hijo: NUNCA pregunta al humano (no tiene ask_fn): en modo ask/strict del padre corre en
|
|
19
|
+
-- strict (peligroso → denegado con motivo, el hijo lo informa); en yolo hereda yolo.
|
|
20
|
+
--
|
|
21
|
+
-- Least-privilege en dos capas:
|
|
22
|
+
-- 1. el perfil recorta el ALLOW-LIST (plan no tiene write/edit/bash → el modelo ni las ve);
|
|
23
|
+
-- 2. cada tool sigue corriendo con call_tool (sus `require` ∩ programa).
|
|
24
|
+
--
|
|
25
|
+
-- MIGA runtime: un `agent` de Synsema NO ve las tasks ni imports de su módulo — solo sus parámetros de
|
|
26
|
+
-- spawn, las builtins y las tasks TOP-LEVEL del programa de entrada. Por eso el hijo en background es
|
|
27
|
+
-- `agent Child` → llama `lampson_subagent(spec_json)`, una task de UNA línea que chat.syn y web.syn
|
|
28
|
+
-- definen a nivel top: `task lampson_subagent(spec) give agents.run_child_json(spec)`.
|
|
29
|
+
|
|
30
|
+
use "./tools.syn" as tools
|
|
31
|
+
use "./loop.syn" as loop
|
|
32
|
+
use "./provider.syn" as provider
|
|
33
|
+
use "./prompt.syn" as prompt
|
|
34
|
+
use "./mcp.syn" as mcp
|
|
35
|
+
use "./lamps.syn" as lamps
|
|
36
|
+
|
|
37
|
+
export let DIR be ".lampson/agents"
|
|
38
|
+
let MAX_TASKS be 6
|
|
39
|
+
let PARALLEL be 4
|
|
40
|
+
|
|
41
|
+
export let PROFILES be {
|
|
42
|
+
"build": {
|
|
43
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "lamp", "delegate"],
|
|
44
|
+
"steps": 40,
|
|
45
|
+
"addendum": "You are in BUILD mode: you may read, edit, create files and run commands. Delegate exploration of large codebases to the `explore` agent and independent code review to the `review` agent when it saves context; several independent questions can go in one delegate call (they run in parallel)."
|
|
46
|
+
},
|
|
47
|
+
"plan": {
|
|
48
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "memory", "todo", "skill", "delegate"],
|
|
49
|
+
"steps": 30,
|
|
50
|
+
"addendum": "You are in PLAN mode: READ-ONLY. You cannot edit files or run commands (those tools are not available). Investigate the codebase, then answer with a concrete, numbered implementation plan: files to touch, what changes in each, risks, and how to verify. Do not write code beyond short illustrative snippets."
|
|
51
|
+
},
|
|
52
|
+
"review": {
|
|
53
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "bash", "process", "memory", "skill"],
|
|
54
|
+
"steps": 25,
|
|
55
|
+
"addendum": "You are a REVIEWER: read-only on files (no write/edit), but you may run commands (tests, linters, git diff). Find real bugs, missing error handling, security issues and violations of the project's conventions. Report findings as a list with file:line, severity and a suggested fix. Do not modify files."
|
|
56
|
+
},
|
|
57
|
+
"explore": {
|
|
58
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "memory", "skill"],
|
|
59
|
+
"steps": 20,
|
|
60
|
+
"addendum": "You are an EXPLORER: read-only. Locate the code relevant to the question (files, functions, line numbers) and report it concisely with paths and short excerpts. Do not propose changes."
|
|
61
|
+
},
|
|
62
|
+
"worker": {
|
|
63
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill"],
|
|
64
|
+
"steps": 40,
|
|
65
|
+
"addendum": "You are a WORKER sub-agent: implement exactly the scoped task you were given (read, edit, run tests), then report what you changed (files) and how you verified it. Stay inside the scope; if something outside it is needed, report it instead of doing it."
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let CHILD_NOTE be "\n\n# Sub-agent rules\n- You are a delegated sub-agent. Your permission scope was fixed when you started and you cannot ask the user: a denied action stays denied — do not retry it; state the limitation in your report so the delegating agent can handle it.\n- Your final message is your REPORT: concrete, with paths and line numbers (or test output) the parent can verify. No questions."
|
|
70
|
+
|
|
71
|
+
-- perfiles que el usuario puede elegir (selector web, /agent). `worker` es interno: el perfil de los
|
|
72
|
+
-- subagentes que `build` lanza con delegate (sin delegate ni mcp para que un hijo no delegue a su vez).
|
|
73
|
+
export task names()
|
|
74
|
+
give where(keys(PROFILES), (n) => n != "worker")
|
|
75
|
+
|
|
76
|
+
export task profile(name)
|
|
77
|
+
when contains(PROFILES, name)
|
|
78
|
+
give PROFILES[name]
|
|
79
|
+
give PROFILES["build"]
|
|
80
|
+
|
|
81
|
+
-- pasos por turno: LAMPSON_MAX_STEPS (si está definido) gana; si no, el del perfil
|
|
82
|
+
export task steps_for(name)
|
|
83
|
+
require env("LAMPSON_*")
|
|
84
|
+
let e be env("LAMPSON_MAX_STEPS", "")
|
|
85
|
+
when e != ""
|
|
86
|
+
give floor(number(e))
|
|
87
|
+
give profile(name)["steps"]
|
|
88
|
+
|
|
89
|
+
export task addendum_for(name)
|
|
90
|
+
give profile(name)["addendum"]
|
|
91
|
+
|
|
92
|
+
-- tools MCP: build/worker ven todas; plan/review/explore solo las de lectura (annotations.readOnlyHint)
|
|
93
|
+
task mcp_readonly_only(name)
|
|
94
|
+
give not (name == "build" or name == "worker")
|
|
95
|
+
|
|
96
|
+
export task registry_for(name)
|
|
97
|
+
require file(".lampson")
|
|
98
|
+
require file(".lampson/*")
|
|
99
|
+
require file("workspace")
|
|
100
|
+
require file("workspace/*")
|
|
101
|
+
require env("LAMPSON_*")
|
|
102
|
+
let p be profile(name)
|
|
103
|
+
let reg be tools.registry_subset(p["tools"])
|
|
104
|
+
when contains(p["tools"], "delegate")
|
|
105
|
+
set reg["delegate"] to delegate
|
|
106
|
+
each m in mcp.names(mcp_readonly_only(name))
|
|
107
|
+
set reg[m] to "mcp"
|
|
108
|
+
each m in lamps.names(mcp_readonly_only(name))
|
|
109
|
+
set reg[m] to "lamp"
|
|
110
|
+
give reg
|
|
111
|
+
|
|
112
|
+
export task catalog_for(name)
|
|
113
|
+
require file(".lampson")
|
|
114
|
+
require file(".lampson/*")
|
|
115
|
+
require file("workspace")
|
|
116
|
+
require file("workspace/*")
|
|
117
|
+
require env("LAMPSON_*")
|
|
118
|
+
let p be profile(name)
|
|
119
|
+
let cat be tools.catalog_subset(p["tools"])
|
|
120
|
+
when contains(p["tools"], "delegate")
|
|
121
|
+
set cat to append(cat, DELEGATE_SPEC)
|
|
122
|
+
give cat + mcp.catalog(mcp_readonly_only(name)) + lamps.catalog(mcp_readonly_only(name))
|
|
123
|
+
|
|
124
|
+
-- Info de entorno para el system prompt (compartida por chat.syn / web.syn / delegate)
|
|
125
|
+
export task env_info(cfg)
|
|
126
|
+
require env("LAMPSON_*")
|
|
127
|
+
require env("OS")
|
|
128
|
+
require time
|
|
129
|
+
let sc be tools.shell_config()
|
|
130
|
+
-- desde dónde habla el usuario (blackboard "lampson:ui", lo comparte el entry): el modelo NO debe decirle
|
|
131
|
+
-- "/lamps" a alguien que está en la web (2026-08-28)
|
|
132
|
+
observe "lampson:ui" as ui
|
|
133
|
+
give {
|
|
134
|
+
"ui": when ui == nothing then "terminal" otherwise text(ui["kind"]),
|
|
135
|
+
"cwd": env("LAMPSON_WORKSPACE", "./workspace"),
|
|
136
|
+
"os": env("OS", "unix"),
|
|
137
|
+
"shell": sc["shell"],
|
|
138
|
+
"date": format_time(now(), "%Y-%m-%d"),
|
|
139
|
+
"model": cfg["model"],
|
|
140
|
+
"provider": cfg["provider"]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
-- ---------- estado de los hijos: archivos en .lampson/agents ----------
|
|
144
|
+
|
|
145
|
+
task path(id, ext)
|
|
146
|
+
give DIR + "/" + id + "." + ext
|
|
147
|
+
|
|
148
|
+
task read_json(id)
|
|
149
|
+
try
|
|
150
|
+
give json_decode(read_file(path(id, "json")))
|
|
151
|
+
recover err
|
|
152
|
+
give nothing
|
|
153
|
+
|
|
154
|
+
task write_json(id, m)
|
|
155
|
+
write_file(path(id, "json"), json_encode(m))
|
|
156
|
+
|
|
157
|
+
task one_line(s, max)
|
|
158
|
+
let t be replace_text(replace_text(text(s), "\r", ""), "\n", " ⏎ ")
|
|
159
|
+
when length(t) > max
|
|
160
|
+
give slice(t, 0, max) + "…"
|
|
161
|
+
give t
|
|
162
|
+
|
|
163
|
+
-- log vivo del hijo (una línea por evento) — el padre y el humano lo pueden mirar mientras corre
|
|
164
|
+
export task child_event(kind, data, tag)
|
|
165
|
+
require file(".lampson")
|
|
166
|
+
require file(".lampson/*")
|
|
167
|
+
let line be ""
|
|
168
|
+
when kind == "assistant"
|
|
169
|
+
set line to "[assistant] " + one_line(data, 600)
|
|
170
|
+
otherwise when kind == "tool_call"
|
|
171
|
+
set line to "⚙ " + data["name"] + " " + one_line(json_encode(data["args"]), 300)
|
|
172
|
+
otherwise when kind == "tool_result"
|
|
173
|
+
let out be data["output"]
|
|
174
|
+
let bad be starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
175
|
+
set line to (when bad then "✗ " otherwise "✓ ") + one_line(out, 300)
|
|
176
|
+
otherwise when kind == "inbox"
|
|
177
|
+
set line to "[inbox] " + one_line(data, 300)
|
|
178
|
+
otherwise when kind == "error"
|
|
179
|
+
set line to "‼ " + one_line(data, 300)
|
|
180
|
+
otherwise when kind == "compact"
|
|
181
|
+
set line to "⧗ compact"
|
|
182
|
+
when line != ""
|
|
183
|
+
append_file(path(tag, "log"), line + "\n")
|
|
184
|
+
bus_publish("subagent." + tag, {"kind": kind, "line": line})
|
|
185
|
+
|
|
186
|
+
-- buzón del hijo: steer (mensajes del padre) y stop
|
|
187
|
+
export task child_inbox(step, tag)
|
|
188
|
+
require file(".lampson")
|
|
189
|
+
require file(".lampson/*")
|
|
190
|
+
let stop_now be false
|
|
191
|
+
try
|
|
192
|
+
read_file(path(tag, "stop"))
|
|
193
|
+
set stop_now to true
|
|
194
|
+
recover err
|
|
195
|
+
set stop_now to false
|
|
196
|
+
let msgs be []
|
|
197
|
+
try
|
|
198
|
+
let raw be trim(read_file(path(tag, "steer")))
|
|
199
|
+
when raw != ""
|
|
200
|
+
set msgs to ["[harness] Message from the delegating agent: " + raw]
|
|
201
|
+
write_file(path(tag, "steer"), "")
|
|
202
|
+
recover err
|
|
203
|
+
set msgs to msgs
|
|
204
|
+
when stop_now or length(msgs) > 0
|
|
205
|
+
give {"messages": msgs, "stop": stop_now}
|
|
206
|
+
give nothing
|
|
207
|
+
|
|
208
|
+
-- buzón del padre: hijos en background terminados y no entregados
|
|
209
|
+
export task parent_inbox(step, tag)
|
|
210
|
+
require file(".lampson")
|
|
211
|
+
require file(".lampson/*")
|
|
212
|
+
let msgs be []
|
|
213
|
+
each c in list_children()
|
|
214
|
+
when c["status"] != "running" and not c["delivered"]
|
|
215
|
+
set msgs to append(msgs, notice_text(c))
|
|
216
|
+
let m be read_json(c["id"])
|
|
217
|
+
set m["delivered"] to true
|
|
218
|
+
write_json(c["id"], m)
|
|
219
|
+
when length(msgs) > 0
|
|
220
|
+
give {"messages": msgs, "stop": false}
|
|
221
|
+
give nothing
|
|
222
|
+
|
|
223
|
+
export task notice_text(c)
|
|
224
|
+
give "[harness] Sub-agent " + c["id"] + " (" + c["agent"] + ") finished: " + c["status"] + " · " + text(c["steps"]) + " steps · " + text(c["tokens"]) + " tokens.\nTask: " + one_line(c["brief"], 200) + "\nReport (self-report — verify before relying on it):\n" + c["text"]
|
|
225
|
+
|
|
226
|
+
task ends_with(s, suffix)
|
|
227
|
+
when length(s) < length(suffix)
|
|
228
|
+
give false
|
|
229
|
+
give slice(s, length(s) - length(suffix), length(s)) == suffix
|
|
230
|
+
|
|
231
|
+
-- [{id, agent, brief, status, steps, tokens, started, finished, delivered, text}]
|
|
232
|
+
export task list_children()
|
|
233
|
+
require file(".lampson")
|
|
234
|
+
require file(".lampson/*")
|
|
235
|
+
let out be []
|
|
236
|
+
try
|
|
237
|
+
each e in list_dir(DIR)
|
|
238
|
+
when ends_with(e["name"], ".json")
|
|
239
|
+
let m be read_json(slice(e["name"], 0, length(e["name"]) - 5))
|
|
240
|
+
when m != nothing
|
|
241
|
+
set out to append(out, m)
|
|
242
|
+
recover err
|
|
243
|
+
give []
|
|
244
|
+
give sort_by(out, (x) => x["started"])
|
|
245
|
+
|
|
246
|
+
-- poda del historial: los terminados hace más de `max_age_s` segundos, y los que sobran por encima de
|
|
247
|
+
-- `keep` terminados (los más viejos primero). No hay builtin de borrado → rm por bash con ids validados.
|
|
248
|
+
export task prune(keep, max_age_s)
|
|
249
|
+
require exec
|
|
250
|
+
require time
|
|
251
|
+
require env("LAMPSON_*")
|
|
252
|
+
require env("OS")
|
|
253
|
+
require file(".lampson")
|
|
254
|
+
require file(".lampson/*")
|
|
255
|
+
-- un "running" de hace más de 6 h es un fantasma (lampson cerrado con el hijo vivo, o un crash viejo)
|
|
256
|
+
let finished be where(list_children(), (c) => c["status"] != "running" or now() - c["started"] > 21600)
|
|
257
|
+
let victims be []
|
|
258
|
+
let n be length(finished)
|
|
259
|
+
let i be 0
|
|
260
|
+
each c in finished
|
|
261
|
+
set i to i + 1
|
|
262
|
+
let fin be when c["finished"] == nothing then c["started"] otherwise c["finished"]
|
|
263
|
+
let old be false
|
|
264
|
+
when fin != nothing
|
|
265
|
+
set old to now() - fin > max_age_s
|
|
266
|
+
when (old or n - i >= keep) and matches(c["id"], "[a-z]+-[0-9]+")
|
|
267
|
+
set victims to append(victims, c["id"])
|
|
268
|
+
when length(victims) > 0
|
|
269
|
+
let files be []
|
|
270
|
+
each v in victims
|
|
271
|
+
each ext in ["json", "log", "steer", "stop"]
|
|
272
|
+
set files to append(files, "'" + DIR + "/" + v + "." + ext + "'")
|
|
273
|
+
let is_win be env("OS", "") == "Windows_NT"
|
|
274
|
+
let sh be env("LAMPSON_SHELL", when is_win then "C:\Program Files\Git\bin\bash.exe" otherwise "bash")
|
|
275
|
+
run(sh, ["-c", "rm -f " + join(files, " ")], 30, {"cwd": "."})
|
|
276
|
+
give length(victims)
|
|
277
|
+
|
|
278
|
+
-- pedir a todos los hijos vivos que paren (salida de lampson)
|
|
279
|
+
export task stop_all()
|
|
280
|
+
require file(".lampson")
|
|
281
|
+
require file(".lampson/*")
|
|
282
|
+
each c in list_children()
|
|
283
|
+
when c["status"] == "running"
|
|
284
|
+
write_file(path(c["id"], "stop"), "stop")
|
|
285
|
+
|
|
286
|
+
-- ---------- correr UN hijo hasta el final (foreground: parallel_map · background: agent Child) ----------
|
|
287
|
+
|
|
288
|
+
export task run_child(spec)
|
|
289
|
+
require net
|
|
290
|
+
require time
|
|
291
|
+
require exec
|
|
292
|
+
require env("LAMPSON_*")
|
|
293
|
+
require env("OS")
|
|
294
|
+
require secret("LAMPSON_*")
|
|
295
|
+
require file("workspace")
|
|
296
|
+
require file("workspace/*")
|
|
297
|
+
require file.read("skills")
|
|
298
|
+
require file.read("skills/*")
|
|
299
|
+
require file("memory")
|
|
300
|
+
require file("memory/*")
|
|
301
|
+
require file(".lampson")
|
|
302
|
+
require file(".lampson/*")
|
|
303
|
+
let id be spec["id"]
|
|
304
|
+
let agent be spec["agent"]
|
|
305
|
+
let p be PROFILES[agent]
|
|
306
|
+
let cfg be provider.config()
|
|
307
|
+
write_json(id, {"id": id, "agent": agent, "brief": spec["brief"], "status": "running", "steps": 0, "tokens": 0, "started": spec["started"], "finished": nothing, "delivered": not spec["background"], "text": "", "background": spec["background"]})
|
|
308
|
+
write_file(path(id, "log"), "# sub-agent " + id + " (" + agent + ")\n# " + one_line(spec["brief"], 300) + "\n")
|
|
309
|
+
-- TODO lo que puede fallar va dentro del try: si algo revienta (prompt, provider, loop) el hijo queda
|
|
310
|
+
-- en status "error" y no como un "running" fantasma (pasó 2026-08-28 con un prompt.syn roto)
|
|
311
|
+
let result be nothing
|
|
312
|
+
try
|
|
313
|
+
let system be {"role": "system", "content": prompt.build(env_info(cfg), p["addendum"] + CHILD_NOTE)}
|
|
314
|
+
let full_brief be when spec["context"] == nothing or spec["context"] == "" then spec["brief"] otherwise spec["brief"] + "\n\nContext from the delegating agent:\n" + spec["context"]
|
|
315
|
+
let messages be [system, {"role": "user", "content": full_brief}]
|
|
316
|
+
let opts be loop.default_opts(tools.registry_subset(p["tools"]), tools.catalog_subset(p["tools"]), nothing)
|
|
317
|
+
set opts to loop.with_steps(opts, p["steps"])
|
|
318
|
+
set opts to loop.with_mode(opts, when spec["mode"] == "yolo" then "yolo" otherwise "strict")
|
|
319
|
+
set opts to loop.with_inbox(opts, child_inbox, id)
|
|
320
|
+
set result to loop.run_turn(cfg, messages, opts, child_event)
|
|
321
|
+
recover err
|
|
322
|
+
append_file(path(id, "log"), "‼ " + one_line(err, 300) + "\n")
|
|
323
|
+
set result to {"text": "(the sub-agent crashed: " + one_line(err, 300) + ")", "steps": 0, "usage": {"input": 0, "output": 0}, "stopped": "error"}
|
|
324
|
+
let status be when result["stopped"] == "done" then "done" otherwise result["stopped"]
|
|
325
|
+
let tokens be result["usage"]["input"] + result["usage"]["output"]
|
|
326
|
+
let final be {"id": id, "agent": agent, "brief": spec["brief"], "status": status, "steps": result["steps"], "tokens": tokens, "started": spec["started"], "finished": now(), "delivered": not spec["background"], "text": when result["text"] == "" then "(the sub-agent produced no final text)" otherwise result["text"], "background": spec["background"]}
|
|
327
|
+
write_json(id, final)
|
|
328
|
+
append_file(path(id, "log"), "# finished: " + status + " · " + text(result["steps"]) + " steps · " + text(tokens) + " tokens\n")
|
|
329
|
+
bus_publish("subagent.done", {"id": id, "status": status})
|
|
330
|
+
give final
|
|
331
|
+
|
|
332
|
+
-- entrada para el agente en background (recibe JSON porque los parámetros del spawn se copian como valores)
|
|
333
|
+
export task run_child_json(spec_json)
|
|
334
|
+
require net
|
|
335
|
+
require time
|
|
336
|
+
require exec
|
|
337
|
+
require env("LAMPSON_*")
|
|
338
|
+
require env("OS")
|
|
339
|
+
require secret("LAMPSON_*")
|
|
340
|
+
require file("workspace")
|
|
341
|
+
require file("workspace/*")
|
|
342
|
+
require file.read("skills")
|
|
343
|
+
require file.read("skills/*")
|
|
344
|
+
require file("memory")
|
|
345
|
+
require file("memory/*")
|
|
346
|
+
require file(".lampson")
|
|
347
|
+
require file(".lampson/*")
|
|
348
|
+
give run_child(json_decode(spec_json))
|
|
349
|
+
|
|
350
|
+
-- hilo con intérprete propio: solo puede llamar tasks top-level del programa de entrada. Declara sus
|
|
351
|
+
-- capabilities (un agente no hereda las del padre: sin `require` propio, env()/net fallan adentro).
|
|
352
|
+
agent Child
|
|
353
|
+
require net
|
|
354
|
+
require time
|
|
355
|
+
require exec
|
|
356
|
+
require env("LAMPSON_*")
|
|
357
|
+
require env("OS")
|
|
358
|
+
require secret("LAMPSON_*")
|
|
359
|
+
require file("workspace")
|
|
360
|
+
require file("workspace/*")
|
|
361
|
+
require file.read("skills")
|
|
362
|
+
require file.read("skills/*")
|
|
363
|
+
require file("memory")
|
|
364
|
+
require file("memory/*")
|
|
365
|
+
require file(".lampson")
|
|
366
|
+
require file(".lampson/*")
|
|
367
|
+
lampson_subagent(spec_json)
|
|
368
|
+
|
|
369
|
+
task format_report(c)
|
|
370
|
+
give "[" + c["id"] + " · " + c["agent"] + " · " + c["status"] + " · " + text(c["steps"]) + " steps · " + text(c["tokens"]) + " tokens · log: " + DIR + "/" + c["id"] + ".log]\n" + c["text"]
|
|
371
|
+
|
|
372
|
+
-- el hijo nunca pregunta: hereda yolo si el operador lo fijó (LAMPSON_PERMISSION); si no, strict
|
|
373
|
+
task child_mode()
|
|
374
|
+
require env("LAMPSON_*")
|
|
375
|
+
give when lower(env("LAMPSON_PERMISSION", "ask")) == "yolo" then "yolo" otherwise "strict"
|
|
376
|
+
|
|
377
|
+
-- id único aun para tasks creadas en el mismo milisegundo (índice dentro del batch)
|
|
378
|
+
task new_id(agent, i)
|
|
379
|
+
give agent + "-" + slice(text(floor(now() * 1000)), 7, 13) + text(i)
|
|
380
|
+
|
|
381
|
+
-- ---------- delegate (tool) ----------
|
|
382
|
+
|
|
383
|
+
export task delegate(agent, brief, context, tasks, background, action, id, message)
|
|
384
|
+
require net
|
|
385
|
+
require time
|
|
386
|
+
require exec
|
|
387
|
+
require env("LAMPSON_*")
|
|
388
|
+
require env("OS")
|
|
389
|
+
require secret("LAMPSON_*")
|
|
390
|
+
require file("workspace")
|
|
391
|
+
require file("workspace/*")
|
|
392
|
+
require file.read("skills")
|
|
393
|
+
require file.read("skills/*")
|
|
394
|
+
require file("memory")
|
|
395
|
+
require file("memory/*")
|
|
396
|
+
require file(".lampson")
|
|
397
|
+
require file(".lampson/*")
|
|
398
|
+
let act be when action == nothing or action == "" then "spawn" otherwise action
|
|
399
|
+
when act == "list"
|
|
400
|
+
let lines be []
|
|
401
|
+
each c in list_children()
|
|
402
|
+
set lines to append(lines, c["id"] + " " + c["status"] + " " + text(c["steps"]) + " steps " + one_line(c["brief"], 80))
|
|
403
|
+
give when length(lines) == 0 then "no sub-agents yet" otherwise join(lines, "\n")
|
|
404
|
+
when act == "steer" or act == "stop" or act == "result"
|
|
405
|
+
let c be when id == nothing then nothing otherwise read_json(id)
|
|
406
|
+
when c == nothing
|
|
407
|
+
raise("unknown sub-agent id '" + text(id) + "' (use action=list)")
|
|
408
|
+
when act == "result"
|
|
409
|
+
give format_report(c)
|
|
410
|
+
when c["status"] != "running"
|
|
411
|
+
give "sub-agent " + id + " already finished (" + c["status"] + "); use action=result to read its report"
|
|
412
|
+
when act == "stop"
|
|
413
|
+
write_file(path(id, "stop"), "stop")
|
|
414
|
+
give "stop requested for " + id + "; its partial report will arrive in your inbox"
|
|
415
|
+
when message == nothing or message == ""
|
|
416
|
+
raise("steer needs a message")
|
|
417
|
+
append_file(path(id, "steer"), message + "\n")
|
|
418
|
+
give "message queued for " + id + " (it reads it before its next step)"
|
|
419
|
+
-- spawn
|
|
420
|
+
prune(10, 1800)
|
|
421
|
+
let items be [{"agent": agent, "brief": brief, "context": context}]
|
|
422
|
+
when tasks != nothing
|
|
423
|
+
when length(tasks) > 0
|
|
424
|
+
set items to tasks
|
|
425
|
+
when length(items) > MAX_TASKS
|
|
426
|
+
raise("at most " + text(MAX_TASKS) + " tasks per delegate call")
|
|
427
|
+
let specs be []
|
|
428
|
+
let i be 0
|
|
429
|
+
each t in items
|
|
430
|
+
set i to i + 1
|
|
431
|
+
let a be when contains(t, "agent") then text(t["agent"]) otherwise ""
|
|
432
|
+
when not contains(PROFILES, a)
|
|
433
|
+
raise("unknown agent '" + a + "'; available: explore, plan, review, worker")
|
|
434
|
+
when a == "build"
|
|
435
|
+
raise("cannot delegate to 'build' (a child never gets `delegate`): use worker for scoped implementation, or explore/plan/review")
|
|
436
|
+
when not contains(t, "brief") or trim(text(t["brief"])) == ""
|
|
437
|
+
raise("every task needs a brief")
|
|
438
|
+
set specs to append(specs, {"id": new_id(a, i), "agent": a, "brief": text(t["brief"]), "context": when contains(t, "context") then text(t["context"]) otherwise "", "mode": child_mode(), "background": background == true, "started": now()})
|
|
439
|
+
when background == true
|
|
440
|
+
let lines be []
|
|
441
|
+
each sp in specs
|
|
442
|
+
write_json(sp["id"], {"id": sp["id"], "agent": sp["agent"], "brief": sp["brief"], "status": "running", "steps": 0, "tokens": 0, "started": sp["started"], "finished": nothing, "delivered": false, "text": "", "background": true})
|
|
443
|
+
write_file(path(sp["id"], "log"), "# starting…\n")
|
|
444
|
+
spawn Child with spec_json = json_encode(sp)
|
|
445
|
+
bus_publish("subagent.started", {"id": sp["id"], "agent": sp["agent"]})
|
|
446
|
+
set lines to append(lines, "started sub-agent " + sp["id"] + " (" + sp["agent"] + ") in the background · live log: " + DIR + "/" + sp["id"] + ".log")
|
|
447
|
+
give join(lines, "\n") + "\nYou will receive each report automatically as a new message when it finishes — keep working on independent steps; do NOT poll or sleep. delegate(action=steer|stop, id=…) to redirect or cut it."
|
|
448
|
+
let results be parallel_map(run_child, specs, PARALLEL)
|
|
449
|
+
let parts be []
|
|
450
|
+
each c in results
|
|
451
|
+
set parts to append(parts, format_report(c))
|
|
452
|
+
give join(parts, "\n\n") + "\n\n(Reports are self-reports: verify paths, diffs and test results before relying on them.)"
|
|
453
|
+
|
|
454
|
+
export let DELEGATE_SPEC be {
|
|
455
|
+
"name": "delegate",
|
|
456
|
+
"description": "Run sub-agents: each gets its own fresh context, a restricted toolset and returns only its final report (it does not see your conversation — give it a self-contained brief). Agents: `explore` (read-only; find where things are), `plan` (read-only; implementation plan), `review` (read-only files, can run tests/commands; find bugs), `worker` (can edit files and run commands: a scoped implementation task). Several tasks in one call run IN PARALLEL and come back consolidated — use that for independent questions or independent changes in different files. background=true returns immediately with ids; each report arrives later as a message in your context (do not poll). action=list|steer|stop|result manages background sub-agents (steer sends a message the child reads before its next step; stop returns its partial report). Reports are SELF-REPORTS: verify what matters (read the file, run the test) before telling the user it is done. Sub-agents cannot ask the user and cannot delegate.",
|
|
457
|
+
"parameters": {"type": "object", "properties": {
|
|
458
|
+
"tasks": {"type": "array", "description": "Batch: [{agent, brief, context?}] — they run in parallel (max 6)", "items": {"type": "object", "properties": {
|
|
459
|
+
"agent": {"type": "string", "enum": ["explore", "plan", "review", "worker"]},
|
|
460
|
+
"brief": {"type": "string"},
|
|
461
|
+
"context": {"type": "string"}
|
|
462
|
+
}, "required": ["agent", "brief"]}},
|
|
463
|
+
"agent": {"type": "string", "enum": ["explore", "plan", "review", "worker"], "description": "Single task form (alternative to tasks)"},
|
|
464
|
+
"brief": {"type": "string", "description": "Self-contained instructions for the sub-agent"},
|
|
465
|
+
"context": {"type": "string", "description": "Optional extra context (relevant paths, findings so far)"},
|
|
466
|
+
"background": {"type": "boolean", "description": "true: return immediately; reports arrive later as messages"},
|
|
467
|
+
"action": {"type": "string", "enum": ["spawn", "list", "steer", "stop", "result"], "description": "default spawn"},
|
|
468
|
+
"id": {"type": "string", "description": "steer/stop/result: the sub-agent id"},
|
|
469
|
+
"message": {"type": "string", "description": "steer: text the sub-agent will read before its next step"}
|
|
470
|
+
}}
|
|
471
|
+
}
|