lampson 0.1.0 → 0.1.2
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/README.md +394 -382
- package/bin/lampson.js +8 -2
- package/chat.syn +1029 -799
- package/lampson.cmd +4 -4
- package/lampson.ps1 +88 -88
- package/lampson.sh +42 -42
- package/lib/agents.syn +471 -471
- package/lib/diff.syn +142 -0
- package/lib/lamps.syn +386 -386
- package/lib/line.syn +365 -0
- package/lib/lsp.syn +503 -503
- package/lib/mcp.syn +403 -403
- package/lib/md.syn +171 -0
- package/lib/permission.syn +189 -154
- package/lib/prompt.syn +75 -75
- package/lib/session.syn +111 -111
- package/lib/skills.syn +179 -179
- package/lib/tools/bash.syn +105 -105
- package/lib/tools/edit.syn +35 -32
- package/lib/tools/memo.syn +148 -148
- package/lib/tools/process.syn +46 -46
- package/lib/tools/write.syn +4 -0
- package/lib/tools.syn +198 -198
- package/lib/tree.syn +59 -59
- package/package.json +2 -2
- package/public/index.html +1268 -1268
- package/skills/lampson/SKILL.md +117 -117
- package/skills/synsema/SKILL.md +15 -2
- package/web.syn +468 -468
package/lib/agents.syn
CHANGED
|
@@ -1,471 +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
|
-
}
|
|
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
|
+
}
|