lampson 0.1.1 → 0.1.3
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 +17 -1
- package/chat.syn +387 -25
- package/lib/diff.syn +168 -0
- package/lib/line.syn +370 -0
- package/lib/loop.syn +25 -11
- package/lib/md.syn +229 -0
- package/lib/permission.syn +36 -1
- package/lib/prompt.syn +1 -1
- package/lib/tools/edit.syn +4 -1
- package/lib/tools/write.syn +4 -0
- package/lib/ui.syn +161 -0
- package/package.json +2 -2
- package/public/index.html +11 -1
- package/skills/synsema/SKILL.md +15 -2
package/README.md
CHANGED
|
@@ -132,6 +132,22 @@ In the REPL, `/` lists every command (`/agent`, `/ask` `/yolo` `/strict`, `/mode
|
|
|
132
132
|
`/files`, `/procs`, `/logs <name>`, `/stop <name>`, `/kill <pid>`, `/skills`, `/sessions`,
|
|
133
133
|
`/resume <id>`, `/tokens`, `/flags`). `!cmd` runs a command yourself.
|
|
134
134
|
|
|
135
|
+
The prompt is a real line editor (Synsema ≥ 0.6.11, falls back to plain `read_line` without a TTY):
|
|
136
|
+
typing `/` opens the command menu (recent ones first, filtered as you type), `Tab` completes the
|
|
137
|
+
command and then its arguments (files for `/image`, sessions, providers, processes, lamps, MCP/LSP
|
|
138
|
+
servers, flags), `↑↓` browse history or the menu, `Alt+Enter` inserts a newline, `Ctrl+O` shows the
|
|
139
|
+
last tool result in full, `Ctrl+U`/`Ctrl+W` clear the line/word, `Esc` closes the menu. Approvals are
|
|
140
|
+
an arrow-key menu (`permitir`/`denegar`, or `p`/`d`).
|
|
141
|
+
|
|
142
|
+
While the model thinks or a slow tool runs (a long `bash`, a sub-agent), a status line shows what is
|
|
143
|
+
running, the elapsed time and a bar that fills as you wait (`⠋ pensando ▰▰▱▱▱▱▱▱▱▱ 12s`); it appears
|
|
144
|
+
after 0.4 s so instant tools do not flicker, and anything you type meanwhile lands in the prompt.
|
|
145
|
+
|
|
146
|
+
The terminal renders the model's markdown (headings, lists, tables, code fences) and shows every tool
|
|
147
|
+
result: `edit`/`write` print a line diff (`- red / + green`, line numbers, 2 lines of context); other
|
|
148
|
+
tools are collapsed to 15 lines. `/out [n]` prints the n-th last result of the turn in full and
|
|
149
|
+
`/verbose` toggles full output for every tool (saved in `.lampson/config.json`).
|
|
150
|
+
|
|
135
151
|
The project is mounted as `lampson/workspace` (an NTFS junction on Windows, a symlink elsewhere)
|
|
136
152
|
and every tool declares `file("workspace/*")` — that literal, named scope is what makes the
|
|
137
153
|
confinement real. Config, sessions and process logs live in the `lampson` folder, never in your project.
|
|
@@ -287,7 +303,7 @@ Borrowed from the harness that does each part best (see `notes/*.md`):
|
|
|
287
303
|
| Budget runs out silently | 80 %: a notice appended to the latest tool result (no new user message, cache stays warm); 95 %: last step without tools, summary required | hermes / opencode |
|
|
288
304
|
| Edits a file it never read, or one that changed | **Observation gate in code**: `read` records the file hash; `edit`/`write` on an existing file are rejected without it, or if the file changed since | deepseek |
|
|
289
305
|
| Loses the plan | `todo` tool (whole-list replacement, one `in_progress` at a time, scoped to the session like the three references); re-injected only after context compaction, active items only | hermes, opencode |
|
|
290
|
-
| Reads the whole project before touching anything | **Exploration cap** (ours): after
|
|
306
|
+
| Reads the whole project before touching anything | **Exploration cap** (ours): after 12 read-only calls in a row (read/ls/find/grep) without an edit/write/command the result carries a warning; after 24 they are refused until it acts (`LAMPSON_EXPLORE_CAP`) | — |
|
|
291
307
|
|
|
292
308
|
### Sub-agents
|
|
293
309
|
|
package/chat.syn
CHANGED
|
@@ -43,6 +43,10 @@ use "./lib/tools/memo.syn" as memo
|
|
|
43
43
|
use "./lib/update.syn" as update
|
|
44
44
|
use "./lib/settings.syn" as settings
|
|
45
45
|
use "./lib/trace.syn" as trace
|
|
46
|
+
use "./lib/md.syn" as md
|
|
47
|
+
use "./lib/diff.syn" as diff
|
|
48
|
+
use "./lib/line.syn" as ed
|
|
49
|
+
use "./lib/ui.syn" as ui
|
|
46
50
|
use "./lib/mcp.syn" as mcp
|
|
47
51
|
use "./lib/lamps.syn" as lamps
|
|
48
52
|
use "./lib/lsp.syn" as lsp
|
|
@@ -53,6 +57,12 @@ use "./lib/tools/todo.syn" as todo
|
|
|
53
57
|
|
|
54
58
|
let ESC be decode(bytes("1b", "hex"))
|
|
55
59
|
let COLOR be env("LAMPSON_NO_COLOR", "") == ""
|
|
60
|
+
-- /verbose: mostrar el output completo de cada tool (persistido en .lampson/config.json); si no, se
|
|
61
|
+
-- colapsa a OUT_LINES líneas y /out [n] muestra el resultado n-ésimo (desde el último) del turno completo
|
|
62
|
+
let saved_cfg be settings.load()
|
|
63
|
+
let VERBOSE be contains(saved_cfg, "verbose") and saved_cfg["verbose"] == true
|
|
64
|
+
let OUT_LINES be 15
|
|
65
|
+
let turn_outputs be []
|
|
56
66
|
|
|
57
67
|
task c(code, s)
|
|
58
68
|
when not COLOR
|
|
@@ -86,37 +96,201 @@ task summarize(name, out)
|
|
|
86
96
|
let lines be split(out, "\n")
|
|
87
97
|
when starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
88
98
|
give first_line(out, 140)
|
|
89
|
-
when name == "read"
|
|
99
|
+
when name == "read" and not VERBOSE
|
|
90
100
|
give text(length(lines)) + " líneas"
|
|
91
|
-
when name == "ls" or name == "find"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
when (name == "ls" or name == "find") and not VERBOSE
|
|
102
|
+
-- las primeras entradas en una línea (hasta ~100 chars), y cuántas más hay
|
|
103
|
+
let shown be []
|
|
104
|
+
let used be 0
|
|
105
|
+
each l in lines
|
|
106
|
+
when used < 100 and trim(l) != ""
|
|
107
|
+
set shown to append(shown, trim(l))
|
|
108
|
+
set used to used + length(l) + 2
|
|
109
|
+
let rest be length(lines) - length(shown)
|
|
110
|
+
give text(length(lines)) + (when name == "ls" then " entradas: " otherwise " archivos: ") + join(shown, " ") + (when rest > 0 then dim(" +" + text(rest)) otherwise "")
|
|
111
|
+
when name == "delegate" and not VERBOSE
|
|
112
|
+
-- una línea por subagente: el encabezado [id · agente · estado · pasos · tokens · log]
|
|
113
|
+
let heads be []
|
|
114
|
+
each l in lines
|
|
115
|
+
when starts_with(trim(l), "[") and contains(l, " steps") or (starts_with(trim(l), "[") and contains(l, " · log:"))
|
|
116
|
+
set heads to append(heads, trim(l))
|
|
117
|
+
when length(heads) == 0
|
|
118
|
+
give first_line(out, 120)
|
|
119
|
+
give join(heads, "\n ")
|
|
120
|
+
when name == "grep" and not VERBOSE
|
|
121
|
+
let files be {}
|
|
122
|
+
each l in lines
|
|
123
|
+
let f be split(l, ":")[0]
|
|
124
|
+
when trim(l) != ""
|
|
125
|
+
set files[f] to true
|
|
126
|
+
give text(length(lines)) + " coincidencias en " + text(length(keys(files))) + " archivo" + (when length(keys(files)) == 1 then "" otherwise "s") + " · " + first_line(out, 90)
|
|
127
|
+
when name == "bash" or name == "process" or VERBOSE
|
|
128
|
+
-- hasta OUT_LINES líneas del output (todas con /verbose), el resto colapsado → /out
|
|
95
129
|
let shown be []
|
|
96
130
|
each e in enumerate(lines)
|
|
97
|
-
when e["index"] <
|
|
98
|
-
when
|
|
99
|
-
|
|
100
|
-
let extra be when length(lines) > 5 then dim(" (+" + text(length(lines) - 5) + " líneas)") otherwise ""
|
|
131
|
+
when VERBOSE or e["index"] < OUT_LINES
|
|
132
|
+
set shown to append(shown, when VERBOSE then e["item"] otherwise slice(e["item"], 0, 160))
|
|
133
|
+
let extra be when not VERBOSE and length(lines) > OUT_LINES then "\n " + dim("(+" + text(length(lines) - OUT_LINES) + " líneas · /out para ver todo)") otherwise ""
|
|
101
134
|
give join(shown, "\n ") + extra
|
|
102
135
|
give first_line(out, 120)
|
|
103
136
|
|
|
137
|
+
-- diff de un edit/write (antes/después publicado por la tool en el blackboard) — nothing si no hay
|
|
138
|
+
task diff_of(name, out)
|
|
139
|
+
when name != "edit" and name != "write"
|
|
140
|
+
give nothing
|
|
141
|
+
when starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
142
|
+
give nothing
|
|
143
|
+
observe "lampson:ui:diff" as d
|
|
144
|
+
when d == nothing
|
|
145
|
+
give nothing
|
|
146
|
+
give diff.diff(d["old"], d["new"], 2, COLOR)
|
|
147
|
+
|
|
148
|
+
task print_diff(path, d)
|
|
149
|
+
print_diff_with(" ", path, d)
|
|
150
|
+
|
|
151
|
+
-- lead = lo que va antes del ✓ (la llamada, si se fusionó en la misma línea)
|
|
152
|
+
task print_diff_with(lead, path, d)
|
|
153
|
+
let head be green("✓") + " " + c("34", path) + " " + green("+" + text(d["added"])) + " " + red("−" + text(d["removed"]))
|
|
154
|
+
print(lead + head)
|
|
155
|
+
each l in d["lines"]
|
|
156
|
+
print(" " + l)
|
|
157
|
+
|
|
158
|
+
-- tools "instantáneas": la llamada se imprime junto con su resultado, en una sola línea
|
|
159
|
+
task is_instant(name)
|
|
160
|
+
give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp"
|
|
161
|
+
|
|
162
|
+
let pending_call be ""
|
|
163
|
+
|
|
164
|
+
-- /out: resultado completo de una tool del turno
|
|
165
|
+
task print_full(o)
|
|
166
|
+
when o["diff"] != nothing
|
|
167
|
+
print_diff(o["path"], o["diff"])
|
|
168
|
+
otherwise
|
|
169
|
+
each l in split(o["output"], "\n")
|
|
170
|
+
print(" " + l)
|
|
171
|
+
|
|
172
|
+
-- ---------- indicador de espera ----------
|
|
173
|
+
-- El intérprete principal se bloquea en la llamada al modelo y en cada tool; el que dibuja es un agente
|
|
174
|
+
-- (hilo real) que mira el blackboard: `lampson:busy` = {label, kind, since} mientras hay algo en curso.
|
|
175
|
+
-- Arranca a dibujar recién a los 0,4 s (las tools instantáneas no parpadean) y suelta la terminal
|
|
176
|
+
-- (`lampson:spinner:holding` = false) antes de que el principal vuelva a imprimir.
|
|
177
|
+
agent Spinner
|
|
178
|
+
require time
|
|
179
|
+
let frames be ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
180
|
+
let i be 0
|
|
181
|
+
let drawn be false
|
|
182
|
+
let run be true
|
|
183
|
+
-- MIGA: un agente NO puede abrir la terminal (term_open → nothing) pero sí print+flush. Cada print de un
|
|
184
|
+
-- agente sale con el prefijo "[Spinner_0] " en la línea de abajo: cada frame limpia esa línea (\r ESC[K),
|
|
185
|
+
-- sube una (ESC[1A) y redibuja la suya. Al terminar deja el cursor en la línea del spinner, vacía.
|
|
186
|
+
let clear_here be "\r" + esc + "[K"
|
|
187
|
+
let up be esc + "[1A"
|
|
188
|
+
while run
|
|
189
|
+
observe "lampson:spinner:stop" as st
|
|
190
|
+
observe "lampson:busy" as b
|
|
191
|
+
when st == true
|
|
192
|
+
set run to false
|
|
193
|
+
otherwise when b == nothing
|
|
194
|
+
when drawn
|
|
195
|
+
print(clear_here + up + clear_here + up)
|
|
196
|
+
flush()
|
|
197
|
+
set drawn to false
|
|
198
|
+
share false as "lampson:spinner:holding"
|
|
199
|
+
sleep(0.05)
|
|
200
|
+
otherwise
|
|
201
|
+
let el be now() - b["since"]
|
|
202
|
+
when el >= 0.4
|
|
203
|
+
-- barra de "costo hundido": se llena rápido al principio y cada vez más despacio
|
|
204
|
+
let fill be floor(10 * (1 - exp(0 - el / 40)))
|
|
205
|
+
let bar be ""
|
|
206
|
+
let k be 0
|
|
207
|
+
while k < 10
|
|
208
|
+
set bar to bar + (when k < fill then "▰" otherwise "▱")
|
|
209
|
+
set k to k + 1
|
|
210
|
+
let hint be when el > 90 then " esto está tardando (Ctrl+C corta)" otherwise (when el > 30 then " ya casi" otherwise (when el > 8 then " sigue trabajando" otherwise ""))
|
|
211
|
+
let secs be text(floor(el)) + "s"
|
|
212
|
+
let label be b["label"]
|
|
213
|
+
when length(label) > 60
|
|
214
|
+
set label to slice(label, 0, 59) + "…"
|
|
215
|
+
let frame be frames[i % 10]
|
|
216
|
+
let body be when color then esc + "[36m" + frame + esc + "[0m " + label + " " + esc + "[2m" + bar + " " + secs + hint + esc + "[0m" otherwise frame + " " + label + " " + bar + " " + secs + hint
|
|
217
|
+
when drawn
|
|
218
|
+
print(clear_here + up + clear_here + " " + body)
|
|
219
|
+
otherwise
|
|
220
|
+
share true as "lampson:spinner:holding"
|
|
221
|
+
print(clear_here + " " + body)
|
|
222
|
+
set drawn to true
|
|
223
|
+
flush()
|
|
224
|
+
set i to i + 1
|
|
225
|
+
sleep(0.08)
|
|
226
|
+
when drawn
|
|
227
|
+
print(clear_here + up + clear_here + up)
|
|
228
|
+
flush()
|
|
229
|
+
share false as "lampson:spinner:holding"
|
|
230
|
+
|
|
231
|
+
task busy_on(label, kind)
|
|
232
|
+
share {"label": label, "kind": kind, "since": now()} as "lampson:busy"
|
|
233
|
+
|
|
234
|
+
-- apagar y esperar a que el agente suelte la terminal (máx. 1 s) antes de volver a imprimir
|
|
235
|
+
task busy_off()
|
|
236
|
+
share nothing as "lampson:busy"
|
|
237
|
+
let waited be 0
|
|
238
|
+
let holding be true
|
|
239
|
+
while holding and waited < 100
|
|
240
|
+
observe "lampson:spinner:holding" as hd
|
|
241
|
+
set holding to hd == true
|
|
242
|
+
when holding
|
|
243
|
+
sleep(0.01)
|
|
244
|
+
set waited to waited + 1
|
|
245
|
+
|
|
104
246
|
-- tag = "" para el agente principal; los subagentes en background NO pasan por acá (escriben su log)
|
|
105
247
|
task on_event(kind, data, tag)
|
|
106
248
|
trace.event(sid, kind, data, tag)
|
|
107
|
-
when kind == "
|
|
249
|
+
when kind == "busy"
|
|
250
|
+
busy_on(data["label"], data["kind"])
|
|
251
|
+
otherwise when kind == "idle"
|
|
252
|
+
busy_off()
|
|
253
|
+
otherwise when kind == "assistant"
|
|
254
|
+
-- ● marca el turno del asistente (primera línea con contenido)
|
|
255
|
+
let lines be split(md.render(data, COLOR), "\n")
|
|
256
|
+
let marked be false
|
|
257
|
+
let out_lines be []
|
|
258
|
+
each l in lines
|
|
259
|
+
when not marked and trim(l) != "" and starts_with(l, " ")
|
|
260
|
+
set out_lines to append(out_lines, c("36;1", "● ") + slice(l, 2, length(l)))
|
|
261
|
+
set marked to true
|
|
262
|
+
otherwise
|
|
263
|
+
set out_lines to append(out_lines, l)
|
|
108
264
|
print("")
|
|
109
|
-
print(
|
|
265
|
+
print(join(out_lines, "\n"))
|
|
110
266
|
print("")
|
|
111
267
|
otherwise when kind == "inbox"
|
|
112
268
|
print(" " + cyan("✉ " + first_line(data, 140)))
|
|
113
269
|
otherwise when kind == "tool_call"
|
|
114
|
-
|
|
270
|
+
let desc be permission.describe_call(data["name"], data["args"])
|
|
271
|
+
when is_instant(data["name"])
|
|
272
|
+
set pending_call to desc
|
|
273
|
+
otherwise
|
|
274
|
+
set pending_call to ""
|
|
275
|
+
print(" " + yellow("▸ " + desc))
|
|
115
276
|
otherwise when kind == "tool_result"
|
|
116
277
|
let out be data["output"]
|
|
278
|
+
let name be data["call"]["name"]
|
|
117
279
|
let bad be starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
118
|
-
let
|
|
119
|
-
|
|
280
|
+
let d be diff_of(name, out)
|
|
281
|
+
let path be when d != nothing then text(data["call"]["args"]["path"]) otherwise ""
|
|
282
|
+
set turn_outputs to append(turn_outputs, {"name": name, "args": data["call"]["args"], "output": out, "diff": d, "path": path})
|
|
283
|
+
let lead be when pending_call != "" then " " + yellow("▸ " + pending_call) + " " otherwise " "
|
|
284
|
+
set pending_call to ""
|
|
285
|
+
when d != nothing
|
|
286
|
+
print_diff_with(lead, path, d)
|
|
287
|
+
otherwise
|
|
288
|
+
let mark be when bad then red("✗") otherwise green("✓")
|
|
289
|
+
let s be summarize(name, out)
|
|
290
|
+
-- una sola línea → cortar al ancho; multilínea (bash) se deja
|
|
291
|
+
when not contains(s, "\n")
|
|
292
|
+
set s to ui.cut(s, ui.cols() - ui.width(lead) - 4)
|
|
293
|
+
print(lead + mark + " " + (when bad then red(s) otherwise dim(s)))
|
|
120
294
|
otherwise when kind == "tool_denied"
|
|
121
295
|
set data to data
|
|
122
296
|
otherwise when kind == "error"
|
|
@@ -125,17 +299,23 @@ task on_event(kind, data, tag)
|
|
|
125
299
|
print(" " + yellow("el proveedor rechazó el nombre del modelo (" + cfg["model"] + "): /model sin argumentos lista los válidos, /model <nombre> lo cambia"))
|
|
126
300
|
otherwise when kind == "compact"
|
|
127
301
|
print(" " + dim("⧗ compactando contexto (~" + text(data["before"]) + " tokens)"))
|
|
302
|
+
busy_on("compactando contexto", "llm")
|
|
128
303
|
flush()
|
|
129
304
|
|
|
130
305
|
-- Human in the loop con el `approve` nativo de Synsema: prompt [approve] … (y/n) en TTY; sin TTY deniega
|
|
131
306
|
-- (fail-closed, un agente no puede auto-aprobarse); `within` acota la espera.
|
|
132
307
|
task ask_user(name, args, why)
|
|
308
|
+
busy_off()
|
|
133
309
|
print("")
|
|
134
310
|
print(" " + yellow("⚠ requiere tu aprobación · " + why))
|
|
135
311
|
print(" " + permission.describe_call(name, args))
|
|
136
312
|
flush()
|
|
137
|
-
|
|
138
|
-
|
|
313
|
+
-- menú ↑↓/Enter (o p/d, 1/2, Esc = no) cuando hay TTY; sin TTY, el approve nativo (fail-closed)
|
|
314
|
+
let pick be ed.choose(" ¿permitir?", ["permitir", "denegar"], COLOR)
|
|
315
|
+
when pick == -1
|
|
316
|
+
let ok be approve " ¿permitir?" within 10m
|
|
317
|
+
give ok
|
|
318
|
+
give pick == 0
|
|
139
319
|
|
|
140
320
|
let LINE be " ────────────────────────────────────────────────────────────────────────"
|
|
141
321
|
|
|
@@ -164,6 +344,8 @@ let COMMANDS be [
|
|
|
164
344
|
["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
|
|
165
345
|
["/lamps", "[on <nombre> | off <nombre> | run <lámpara> <tool> [json] | remove <nombre>]", "lámparas (plugins de tools): listar, encender o apagar (global: lampson/lamps/ · proyecto: .lampson/lamps/)"],
|
|
166
346
|
["/lsp", "[add <typescript|python|rust|go|css|html> [--project] | add <nombre> <comando…> --ext .x=lang | remove <nombre>]", "language servers (navegación semántica: symbols/definition/references/hover); arrancan en la primera consulta"],
|
|
347
|
+
["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
|
|
348
|
+
["/verbose", "", "alternar: mostrar SIEMPRE el output completo de cada tool (queda guardado en config.json)"],
|
|
167
349
|
["/trace", "[n]", "traza legible de esta sesión (pasos, tools, tiempos, tokens, errores): .lampson/trace/<sesión>.log"],
|
|
168
350
|
["/tokens", "", "tamaño del contexto y tokens gastados en esta sesión"],
|
|
169
351
|
["/new", "", "empezar una sesión nueva (historial vacío)"],
|
|
@@ -182,10 +364,138 @@ task pad(s, n)
|
|
|
182
364
|
set out to out + " "
|
|
183
365
|
give out
|
|
184
366
|
|
|
367
|
+
-- entrada multilínea sin modo raw (read_key llegará a Synsema): una línea que termina en "\" continúa
|
|
368
|
+
-- en la siguiente; una línea que EMPIEZA con """ abre un bloque que cierra con una línea """ sola
|
|
369
|
+
-- (ideal para pegar código o logs). EOF corta el bloque con lo acumulado.
|
|
370
|
+
-- candidatos para completar el argumento de un comando (Tab / menú del editor de línea).
|
|
371
|
+
-- head = argumentos ya escritos antes del último token; last = lo que se está escribiendo
|
|
372
|
+
task complete_args(cmd, head, last)
|
|
373
|
+
let first be trim(head) == ""
|
|
374
|
+
when cmd == "/image"
|
|
375
|
+
give workspace_paths(last)
|
|
376
|
+
when cmd == "/resume" or cmd == "/delete"
|
|
377
|
+
give apply((s) => s["id"], session.list())
|
|
378
|
+
when cmd == "/provider"
|
|
379
|
+
when first
|
|
380
|
+
give apply((p) => p["name"], provider.providers())
|
|
381
|
+
give []
|
|
382
|
+
when cmd == "/logs" or cmd == "/stop"
|
|
383
|
+
when first
|
|
384
|
+
give apply((p) => p["name"], proc.list())
|
|
385
|
+
give []
|
|
386
|
+
when cmd == "/memory"
|
|
387
|
+
give apply((m) => m["name"], memo.list())
|
|
388
|
+
when cmd == "/lamps"
|
|
389
|
+
when first
|
|
390
|
+
give ["on", "off", "run", "remove"]
|
|
391
|
+
give apply((l) => l["name"], lamps.summary())
|
|
392
|
+
when cmd == "/mcp"
|
|
393
|
+
when first
|
|
394
|
+
give ["add", "remove"]
|
|
395
|
+
when starts_with(trim(head), "remove")
|
|
396
|
+
give apply((m) => m["name"], mcp.summary())
|
|
397
|
+
give ["--project"]
|
|
398
|
+
when cmd == "/lsp"
|
|
399
|
+
when first
|
|
400
|
+
give ["add", "remove"]
|
|
401
|
+
when starts_with(trim(head), "remove")
|
|
402
|
+
give apply((s) => s["name"], lsp.summary())
|
|
403
|
+
when trim(head) == "add"
|
|
404
|
+
give sort_by(keys(lsp.PRESETS), (x) => x)
|
|
405
|
+
give ["--project", "--ext"]
|
|
406
|
+
when cmd == "/agent"
|
|
407
|
+
give ["build", "plan", "review", "explore", "worker"]
|
|
408
|
+
when cmd == "/model"
|
|
409
|
+
give []
|
|
410
|
+
give []
|
|
411
|
+
|
|
412
|
+
-- rutas del workspace que empiezan con el prefijo escrito (directorios con "/" final para seguir completando)
|
|
413
|
+
task workspace_paths(prefix)
|
|
414
|
+
let dir be ""
|
|
415
|
+
let base be prefix
|
|
416
|
+
let cut be -1
|
|
417
|
+
let i be length(prefix) - 1
|
|
418
|
+
while i >= 0 and cut == -1
|
|
419
|
+
when slice(prefix, i, i + 1) == "/" or slice(prefix, i, i + 1) == "\\"
|
|
420
|
+
set cut to i
|
|
421
|
+
set i to i - 1
|
|
422
|
+
when cut >= 0
|
|
423
|
+
set dir to slice(prefix, 0, cut + 1)
|
|
424
|
+
set base to slice(prefix, cut + 1, length(prefix))
|
|
425
|
+
let out be []
|
|
426
|
+
try
|
|
427
|
+
each e in list_dir(when dir == "" then "workspace" otherwise "workspace/" + dir)
|
|
428
|
+
when starts_with(e["name"], base) and e["name"] != ".git"
|
|
429
|
+
set out to append(out, dir + e["name"] + (when e["is_dir"] then "/" otherwise ""))
|
|
430
|
+
recover err
|
|
431
|
+
set out to []
|
|
432
|
+
give sort_by(out, (x) => x)
|
|
433
|
+
|
|
434
|
+
-- mientras el humano no escribe: ¿hay avisos de subagentes en background? → cortar la lectura y
|
|
435
|
+
-- dejar que el bucle principal los inyecte (turno automático)
|
|
436
|
+
task idle()
|
|
437
|
+
let pending be false
|
|
438
|
+
try
|
|
439
|
+
each c in agents.list_children()
|
|
440
|
+
when c["status"] != "running" and not c["delivered"]
|
|
441
|
+
set pending to true
|
|
442
|
+
recover err
|
|
443
|
+
set pending to false
|
|
444
|
+
give pending
|
|
445
|
+
|
|
446
|
+
let input_history be []
|
|
447
|
+
|
|
448
|
+
task read_input(prompt)
|
|
449
|
+
let first be read_line(prompt)
|
|
450
|
+
when first == nothing
|
|
451
|
+
give nothing
|
|
452
|
+
let more be []
|
|
453
|
+
when trim(first) == "\"\"\"" or starts_with(first, "\"\"\"")
|
|
454
|
+
let body be slice(first, 3, length(first))
|
|
455
|
+
when trim(body) != ""
|
|
456
|
+
set more to append(more, body)
|
|
457
|
+
let open be true
|
|
458
|
+
while open
|
|
459
|
+
let l be read_line(dim(" … "))
|
|
460
|
+
when l == nothing or trim(l) == "\"\"\""
|
|
461
|
+
set open to false
|
|
462
|
+
otherwise
|
|
463
|
+
set more to append(more, l)
|
|
464
|
+
give join(more, "\n")
|
|
465
|
+
let cur be first
|
|
466
|
+
while ends_with(cur, "\\")
|
|
467
|
+
set more to append(more, slice(cur, 0, length(cur) - 1))
|
|
468
|
+
let l be read_line(dim(" … "))
|
|
469
|
+
set cur to when l == nothing then "" otherwise l
|
|
470
|
+
set more to append(more, cur)
|
|
471
|
+
give join(more, "\n")
|
|
472
|
+
|
|
473
|
+
-- últimos comandos usados (config.json "recent"): "/" solo los muestra primero
|
|
474
|
+
task remember_command(input)
|
|
475
|
+
let name be split(trim(input), " ")[0]
|
|
476
|
+
when name == "/" or name == "/help"
|
|
477
|
+
give nothing
|
|
478
|
+
let doc be settings.load()
|
|
479
|
+
let old be when contains(doc, "recent") then doc["recent"] otherwise []
|
|
480
|
+
let recent be [name]
|
|
481
|
+
each r in old
|
|
482
|
+
when r != name and length(recent) < 3
|
|
483
|
+
set recent to append(recent, r)
|
|
484
|
+
set doc["recent"] to recent
|
|
485
|
+
settings.save(doc)
|
|
486
|
+
give nothing
|
|
487
|
+
|
|
185
488
|
task help()
|
|
489
|
+
let doc be settings.load()
|
|
490
|
+
let recent be when contains(doc, "recent") then doc["recent"] otherwise []
|
|
186
491
|
print("")
|
|
492
|
+
when length(recent) > 0
|
|
493
|
+
print(" recientes " + join(apply((r) => cyan(r), recent), " "))
|
|
494
|
+
print("")
|
|
187
495
|
print(" Escribí lo que querés hacer en el proyecto. El agente lee, busca, edita y corre comandos con tools")
|
|
188
496
|
print(" acotadas al workspace; cada paso queda a la vista. Los comandos empiezan con /:")
|
|
497
|
+
print(" Teclas: Tab completa · ↑↓ historial/menú · Alt+Enter salto de línea · Ctrl+O último resultado completo · Esc cierra el menú")
|
|
498
|
+
print(" Sin TTY (pipe): terminá la línea con \\ para continuar, o un bloque entre líneas \"\"\".")
|
|
189
499
|
print("")
|
|
190
500
|
each c in COMMANDS
|
|
191
501
|
print(" " + pad(c[0] + " " + c[1], 38) + c[2])
|
|
@@ -238,6 +548,21 @@ task banner(ws, cfg, profile, mode, sid)
|
|
|
238
548
|
print(" workspace " + ws)
|
|
239
549
|
print(" agente " + profile + " permisos " + mode + " modelo " + cfg["model"])
|
|
240
550
|
print(" sesión " + sid + " " + git.summary())
|
|
551
|
+
-- extensiones en una línea (detalle con /lamps, /mcp, /lsp): encendidas/total
|
|
552
|
+
let ls be lamps.summary()
|
|
553
|
+
let lamps_on be length(where(ls, (l) => l["enabled"]))
|
|
554
|
+
let ms be mcp.summary()
|
|
555
|
+
let mcp_on be length(where(ms, (m) => m["status"] == "ready"))
|
|
556
|
+
let ss be lsp.summary()
|
|
557
|
+
let ext be []
|
|
558
|
+
when length(ls) > 0
|
|
559
|
+
set ext to append(ext, (when lamps_on > 0 then green("lamps " + text(lamps_on) + "/" + text(length(ls))) otherwise dim("lamps 0/" + text(length(ls)))))
|
|
560
|
+
when length(ms) > 0
|
|
561
|
+
set ext to append(ext, (when mcp_on == length(ms) then green("mcp " + text(mcp_on) + "/" + text(length(ms))) otherwise red("mcp " + text(mcp_on) + "/" + text(length(ms)))))
|
|
562
|
+
when length(ss) > 0
|
|
563
|
+
set ext to append(ext, dim("lsp " + text(length(ss))))
|
|
564
|
+
when length(ext) > 0
|
|
565
|
+
print(" extras " + join(ext, dim(" · ")) + dim(" (/lamps /mcp /lsp)"))
|
|
241
566
|
let upd be update.line()
|
|
242
567
|
when upd != ""
|
|
243
568
|
print(" " + yellow("⬆ " + upd) + dim(" (o /update acá)"))
|
|
@@ -402,14 +727,13 @@ let system_msg be system_for(profile)
|
|
|
402
727
|
-- servers MCP (globales en .lampson/mcp.json, del proyecto en workspace/.lampson/mcp.json): arrancan antes
|
|
403
728
|
-- de armar el catálogo de tools; hasta 8 s de espera a que estén listos
|
|
404
729
|
let mcp_servers be mcp.start_all(8)
|
|
730
|
+
-- mcp / lsp / lámparas van resumidos en el banner ("extras"); acá solo se avisa lo que está ROTO
|
|
405
731
|
each ms in mcp.summary()
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
each s in lsp.summary()
|
|
409
|
-
print(" " + dim("○ lsp " + s["name"] + " (" + s["scope"] + ") " + join(s["extensions"], " ") + " · arranca en la primera consulta"))
|
|
410
|
-
-- lámparas (plugins de tools, lib/lamps.syn): solo se anuncian; apagadas no entran al catálogo
|
|
732
|
+
when ms["status"] != "ready"
|
|
733
|
+
print(" " + red("○ mcp " + ms["name"] + " " + ms["status"]) + dim(when ms["error"] != nothing then " · " + text(ms["error"]) otherwise ""))
|
|
411
734
|
each l in lamps.summary()
|
|
412
|
-
|
|
735
|
+
when l["error"] != nothing
|
|
736
|
+
print(" " + red("○ lamp " + l["name"] + " rota") + dim(" · " + text(l["error"])))
|
|
413
737
|
let opts be opts_for(profile, lower(env("LAMPSON_PERMISSION", "ask")))
|
|
414
738
|
|
|
415
739
|
-- marca de corrida (blackboard): session.save la estampa en meta.run; reanudar una sesión guardada por
|
|
@@ -438,6 +762,9 @@ when sid == ""
|
|
|
438
762
|
share {"id": sid} as "lampson:session"
|
|
439
763
|
|
|
440
764
|
banner(env_info["cwd"], cfg, profile, opts["permission_mode"], sid)
|
|
765
|
+
share nothing as "lampson:busy"
|
|
766
|
+
share false as "lampson:spinner:stop"
|
|
767
|
+
spawn Spinner with esc = ESC, color = COLOR
|
|
441
768
|
flush()
|
|
442
769
|
|
|
443
770
|
let total_usage be {"input": 0, "output": 0}
|
|
@@ -465,14 +792,24 @@ while running
|
|
|
465
792
|
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
466
793
|
flush()
|
|
467
794
|
otherwise
|
|
468
|
-
|
|
795
|
+
let saved be settings.load()
|
|
796
|
+
let ctx be {"commands": COMMANDS, "recent": (when contains(saved, "recent") then saved["recent"] otherwise []), "history": input_history, "complete": complete_args, "idle": idle, "fallback": read_input}
|
|
797
|
+
set line to ed.read((when length(pending_images) > 0 then dim("📎" + text(length(pending_images)) + " ") otherwise "") + cyan("❯ "), COLOR, ctx)
|
|
798
|
+
when line != nothing and line != ed.INTERRUPT and trim(line) != ""
|
|
799
|
+
set input_history to append(input_history, line)
|
|
469
800
|
when inbox != nothing
|
|
470
801
|
set running to running
|
|
802
|
+
otherwise when line == ed.INTERRUPT
|
|
803
|
+
set running to running
|
|
471
804
|
otherwise when line == nothing
|
|
472
805
|
set running to false
|
|
473
806
|
otherwise
|
|
474
807
|
set followups to 0
|
|
808
|
+
when not starts_with(trim(line), "/")
|
|
809
|
+
set turn_outputs to []
|
|
475
810
|
let input be trim(line)
|
|
811
|
+
when starts_with(input, "/") and length(input) > 1
|
|
812
|
+
remember_command(input)
|
|
476
813
|
when input == ""
|
|
477
814
|
set running to running
|
|
478
815
|
otherwise when input == "/exit" or input == "/quit"
|
|
@@ -491,6 +828,23 @@ while running
|
|
|
491
828
|
print(" " + update.apply())
|
|
492
829
|
otherwise when input == "/config"
|
|
493
830
|
show_config(cfg, env_info["cwd"], profile, mode, opts)
|
|
831
|
+
otherwise when input == "/verbose"
|
|
832
|
+
set VERBOSE to not VERBOSE
|
|
833
|
+
let doc be settings.load()
|
|
834
|
+
set doc["verbose"] to VERBOSE
|
|
835
|
+
settings.save(doc)
|
|
836
|
+
print(" " + (when VERBOSE then green("verbose ON") + dim(" · cada tool muestra su output completo") otherwise dim("verbose OFF · outputs colapsados a " + text(OUT_LINES) + " líneas; /out para ver uno completo")))
|
|
837
|
+
otherwise when starts_with(input, "/out")
|
|
838
|
+
let n be trim(slice(input, 4, length(input)))
|
|
839
|
+
let back be when n == "" then 1 otherwise floor(number(n))
|
|
840
|
+
when length(turn_outputs) == 0
|
|
841
|
+
print(dim(" ninguna tool corrió en este turno"))
|
|
842
|
+
otherwise when back < 1 or back > length(turn_outputs)
|
|
843
|
+
print(dim(" hay " + text(length(turn_outputs)) + " resultados en este turno: /out 1 … /out " + text(length(turn_outputs))))
|
|
844
|
+
otherwise
|
|
845
|
+
let o be turn_outputs[length(turn_outputs) - back]
|
|
846
|
+
print(" " + yellow("▸ " + permission.describe_call(o["name"], o["args"])))
|
|
847
|
+
print_full(o)
|
|
494
848
|
otherwise when starts_with(input, "/trace")
|
|
495
849
|
let n be trim(slice(input, 6, length(input)))
|
|
496
850
|
let lines be when n == "" then 40 otherwise floor(number(n))
|
|
@@ -775,23 +1129,31 @@ while running
|
|
|
775
1129
|
-- catálogo fresco por turno: si el turno anterior conectó/quitó un server MCP, sus tools
|
|
776
1130
|
-- entran/salen acá (con los mismos servers el catálogo es idéntico → el prompt cache no se corta)
|
|
777
1131
|
set opts to opts_for(profile, mode)
|
|
1132
|
+
let t0 be now()
|
|
778
1133
|
let result be loop.run_turn(cfg, messages, opts, on_event)
|
|
779
1134
|
trace.turn_end(sid, result)
|
|
780
1135
|
set messages to result["messages"]
|
|
781
1136
|
set total_usage to {"input": total_usage["input"] + result["usage"]["input"], "output": total_usage["output"] + result["usage"]["output"]}
|
|
782
|
-
let summary be text(result["steps"]) + (when result["steps"] == 1 then " paso" otherwise " pasos") + " · " + fmt_tokens(result["usage"]["input"] + result["usage"]["output"]) + " tokens"
|
|
1137
|
+
let summary be text(result["steps"]) + (when result["steps"] == 1 then " paso" otherwise " pasos") + " · " + fmt_tokens(result["usage"]["input"] + result["usage"]["output"]) + " tokens · " + ui.fmt_duration(now() - t0)
|
|
783
1138
|
when result["stopped"] == "max_steps"
|
|
784
1139
|
set summary to summary + " · " + red("límite de " + text(opts["max_steps"]) + " pasos por turno")
|
|
785
1140
|
print(dim(" ─── " + summary))
|
|
786
1141
|
print(" " + yellow("El agente paró por el límite de pasos, no porque terminó. Escribí «seguí» para que continúe donde quedó, o subí LAMPSON_MAX_STEPS."))
|
|
787
1142
|
otherwise when result["stopped"] != "done"
|
|
788
|
-
|
|
1143
|
+
-- el detalle del error ya salió en la línea ‼; acá solo la causa corta
|
|
1144
|
+
let why be result["stopped"]
|
|
1145
|
+
when starts_with(why, "error: network")
|
|
1146
|
+
set why to "error de red (ver ‼ arriba)"
|
|
1147
|
+
otherwise when starts_with(why, "error:")
|
|
1148
|
+
set why to first_line(why, 60)
|
|
1149
|
+
set summary to summary + " · " + red("detenido: " + why)
|
|
789
1150
|
print(dim(" ─── " + summary))
|
|
790
1151
|
otherwise
|
|
791
1152
|
print(dim(" ─── " + summary))
|
|
792
1153
|
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
793
1154
|
flush()
|
|
794
1155
|
-- los procesos gestionados (servidores) y los subagentes en background mueren con lampson
|
|
1156
|
+
share true as "lampson:spinner:stop"
|
|
795
1157
|
agents.stop_all()
|
|
796
1158
|
mcp.stop_all()
|
|
797
1159
|
lsp.stop_all()
|