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
package/chat.syn
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
-- chat.syn — Lampson por terminal (REPL)
|
|
2
|
+
--
|
|
3
|
+
-- synsema run chat.syn -- sesión nueva
|
|
4
|
+
-- LAMPSON_SESSION=<id> synsema run chat.syn -- reanudar
|
|
5
|
+
--
|
|
6
|
+
-- Comandos: /help /new /sessions /resume <id> /tokens /yolo /ask /strict /agent <build|plan|review|explore> /exit
|
|
7
|
+
--
|
|
8
|
+
-- El ENTRY otorga el superset de capacidades; cada tool declara las suyas y call_tool intersecta.
|
|
9
|
+
-- El proyecto objetivo se monta como ./workspace (ver lampson.ps1 / lampson.sh y lib/tools/common.syn).
|
|
10
|
+
|
|
11
|
+
intent: "Coding agent REPL: the model picks tools from an allow-list, each runs least-privilege in the workspace"
|
|
12
|
+
|
|
13
|
+
require net
|
|
14
|
+
require time
|
|
15
|
+
require stdin
|
|
16
|
+
require exec
|
|
17
|
+
require env("LAMPSON_*")
|
|
18
|
+
require env("OS")
|
|
19
|
+
require secret("LAMPSON_*")
|
|
20
|
+
require file("workspace")
|
|
21
|
+
require file("workspace/*")
|
|
22
|
+
require file.read("skills")
|
|
23
|
+
require file.read("skills/*")
|
|
24
|
+
require file.read("lamps")
|
|
25
|
+
require file.read("lamps/*")
|
|
26
|
+
require file("memory")
|
|
27
|
+
require file("memory/*")
|
|
28
|
+
require file(".lampson")
|
|
29
|
+
require file(".lampson/*")
|
|
30
|
+
|
|
31
|
+
use "./lib/provider.syn" as provider
|
|
32
|
+
use "./lib/tools.syn" as tools
|
|
33
|
+
use "./lib/prompt.syn" as prompt
|
|
34
|
+
use "./lib/loop.syn" as loop
|
|
35
|
+
use "./lib/session.syn" as session
|
|
36
|
+
use "./lib/permission.syn" as permission
|
|
37
|
+
use "./lib/agents.syn" as agents
|
|
38
|
+
use "./lib/tree.syn" as tree
|
|
39
|
+
use "./lib/skills.syn" as skills
|
|
40
|
+
use "./lib/git.syn" as git
|
|
41
|
+
use "./lib/tools/proc.syn" as proc
|
|
42
|
+
use "./lib/tools/memo.syn" as memo
|
|
43
|
+
use "./lib/update.syn" as update
|
|
44
|
+
use "./lib/settings.syn" as settings
|
|
45
|
+
use "./lib/trace.syn" as trace
|
|
46
|
+
use "./lib/mcp.syn" as mcp
|
|
47
|
+
use "./lib/lamps.syn" as lamps
|
|
48
|
+
use "./lib/lsp.syn" as lsp
|
|
49
|
+
use "./lib/tools/todo.syn" as todo
|
|
50
|
+
|
|
51
|
+
-- ---------- UI de terminal ----------
|
|
52
|
+
-- Colores ANSI (Windows Terminal, PowerShell 7, cualquier terminal moderna). LAMPSON_NO_COLOR=1 los apaga.
|
|
53
|
+
|
|
54
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
55
|
+
let COLOR be env("LAMPSON_NO_COLOR", "") == ""
|
|
56
|
+
|
|
57
|
+
task c(code, s)
|
|
58
|
+
when not COLOR
|
|
59
|
+
give s
|
|
60
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
61
|
+
|
|
62
|
+
task cyan(s)
|
|
63
|
+
give c("36;1", s)
|
|
64
|
+
task dim(s)
|
|
65
|
+
give c("2", s)
|
|
66
|
+
task yellow(s)
|
|
67
|
+
give c("33", s)
|
|
68
|
+
task green(s)
|
|
69
|
+
give c("32", s)
|
|
70
|
+
task red(s)
|
|
71
|
+
give c("31", s)
|
|
72
|
+
|
|
73
|
+
task first_line(s, max)
|
|
74
|
+
let f be split(s, "\n")[0]
|
|
75
|
+
when length(f) > max
|
|
76
|
+
give slice(f, 0, max) + "…"
|
|
77
|
+
give f
|
|
78
|
+
|
|
79
|
+
task fmt_tokens(n)
|
|
80
|
+
when n >= 1000
|
|
81
|
+
give text(floor(n / 100) / 10) + "k"
|
|
82
|
+
give text(n)
|
|
83
|
+
|
|
84
|
+
-- resumen de UNA línea del resultado de una tool, según la tool
|
|
85
|
+
task summarize(name, out)
|
|
86
|
+
let lines be split(out, "\n")
|
|
87
|
+
when starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
88
|
+
give first_line(out, 140)
|
|
89
|
+
when name == "read"
|
|
90
|
+
give text(length(lines)) + " líneas"
|
|
91
|
+
when name == "ls" or name == "find" or name == "grep"
|
|
92
|
+
give text(length(lines)) + " resultados · " + first_line(out, 90)
|
|
93
|
+
when name == "bash"
|
|
94
|
+
-- hasta 5 líneas del output, el resto colapsado
|
|
95
|
+
let shown be []
|
|
96
|
+
each e in enumerate(lines)
|
|
97
|
+
when e["index"] < 5
|
|
98
|
+
when trim(e["item"]) != ""
|
|
99
|
+
set shown to append(shown, slice(e["item"], 0, 140))
|
|
100
|
+
let extra be when length(lines) > 5 then dim(" (+" + text(length(lines) - 5) + " líneas)") otherwise ""
|
|
101
|
+
give join(shown, "\n ") + extra
|
|
102
|
+
give first_line(out, 120)
|
|
103
|
+
|
|
104
|
+
-- tag = "" para el agente principal; los subagentes en background NO pasan por acá (escriben su log)
|
|
105
|
+
task on_event(kind, data, tag)
|
|
106
|
+
trace.event(sid, kind, data, tag)
|
|
107
|
+
when kind == "assistant"
|
|
108
|
+
print("")
|
|
109
|
+
print(data)
|
|
110
|
+
print("")
|
|
111
|
+
otherwise when kind == "inbox"
|
|
112
|
+
print(" " + cyan("✉ " + first_line(data, 140)))
|
|
113
|
+
otherwise when kind == "tool_call"
|
|
114
|
+
print(" " + yellow("⚙ " + permission.describe_call(data["name"], data["args"])))
|
|
115
|
+
otherwise when kind == "tool_result"
|
|
116
|
+
let out be data["output"]
|
|
117
|
+
let bad be starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
118
|
+
let mark be when bad then red("✗") otherwise green("✓")
|
|
119
|
+
print(" " + mark + " " + (when bad then red(summarize(data["call"]["name"], out)) otherwise dim(summarize(data["call"]["name"], out))))
|
|
120
|
+
otherwise when kind == "tool_denied"
|
|
121
|
+
set data to data
|
|
122
|
+
otherwise when kind == "error"
|
|
123
|
+
print(" " + red("‼ " + text(data)))
|
|
124
|
+
when contains(lower(text(data)), "model")
|
|
125
|
+
print(" " + yellow("el proveedor rechazó el nombre del modelo (" + cfg["model"] + "): /model sin argumentos lista los válidos, /model <nombre> lo cambia"))
|
|
126
|
+
otherwise when kind == "compact"
|
|
127
|
+
print(" " + dim("⧗ compactando contexto (~" + text(data["before"]) + " tokens)"))
|
|
128
|
+
flush()
|
|
129
|
+
|
|
130
|
+
-- Human in the loop con el `approve` nativo de Synsema: prompt [approve] … (y/n) en TTY; sin TTY deniega
|
|
131
|
+
-- (fail-closed, un agente no puede auto-aprobarse); `within` acota la espera.
|
|
132
|
+
task ask_user(name, args, why)
|
|
133
|
+
print("")
|
|
134
|
+
print(" " + yellow("⚠ requiere tu aprobación · " + why))
|
|
135
|
+
print(" " + permission.describe_call(name, args))
|
|
136
|
+
flush()
|
|
137
|
+
let ok be approve " ¿permitir?" within 10m
|
|
138
|
+
give ok
|
|
139
|
+
|
|
140
|
+
let LINE be " ────────────────────────────────────────────────────────────────────────"
|
|
141
|
+
|
|
142
|
+
-- catálogo de comandos del REPL: [comando, argumentos, descripción]. Es la fuente de /help, de `/` y de las sugerencias.
|
|
143
|
+
let COMMANDS be [
|
|
144
|
+
["/agent", "<build|plan|review|explore>", "cambiar de agente (qué tools ve): build edita · plan solo lee y propone · review revisa y corre tests · explore busca"],
|
|
145
|
+
["/ask", "", "permisos: PREGUNTAR antes de un comando peligroso (rm -rf, git push --force, sudo…)"],
|
|
146
|
+
["/yolo", "", "permisos: PERMITIR comandos peligrosos sin preguntar (los destructivos del sistema siguen bloqueados)"],
|
|
147
|
+
["/strict", "", "permisos: DENEGAR comandos peligrosos"],
|
|
148
|
+
["/model", "<nombre>", "cambiar el modelo para esta sesión (mismo proveedor y key)"],
|
|
149
|
+
["/provider", "[nombre] [modelo]", "cambiar de proveedor (deepseek, anthropic, openai, minimax…); sin args lista cuáles tienen key"],
|
|
150
|
+
["/setup", "", "elegir proveedor, modelo y pegar la API key (queda en lampson/.lampson/config.json, local)"],
|
|
151
|
+
["/paste", "", "adjuntar la imagen del portapapeles al próximo mensaje (copiá una captura y escribí /paste)"],
|
|
152
|
+
["/image", "<ruta>", "adjuntar una imagen (png/jpg/gif/webp) al próximo mensaje; /image sin ruta muestra o quita las pendientes"],
|
|
153
|
+
["/delete", "<id>", "borrar una sesión guardada (ver /sessions)"],
|
|
154
|
+
["/config", "", "ver la configuración vigente (proveedor, modelo, límites, workspace)"],
|
|
155
|
+
["/files", "", "árbol completo del workspace"],
|
|
156
|
+
["/procs", "", "procesos que el agente dejó corriendo (servidores) y su estado"],
|
|
157
|
+
["/logs", "<nombre> [n]", "últimas n líneas del log de un proceso gestionado"],
|
|
158
|
+
["/stop", "<nombre>", "parar un proceso gestionado"],
|
|
159
|
+
["/kill", "<pid>", "matar un proceso NO gestionado (p.ej. un servidor huérfano que ves en /procs)"],
|
|
160
|
+
["/memory", "[nombre]", "notas que el agente guardó sobre ESTE proyecto (memory/<proyecto>/*.md); con nombre, la muestra"],
|
|
161
|
+
["/skills", "", "skills disponibles (instrucciones que el agente puede cargar)"],
|
|
162
|
+
["/agents", "", "subagentes lanzados con delegate: estado, pasos y log de cada uno"],
|
|
163
|
+
["/todo", "", "lista de tareas del agente en esta sesión (la mantiene con la tool todo)"],
|
|
164
|
+
["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
|
|
165
|
+
["/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
|
+
["/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"],
|
|
167
|
+
["/trace", "[n]", "traza legible de esta sesión (pasos, tools, tiempos, tokens, errores): .lampson/trace/<sesión>.log"],
|
|
168
|
+
["/tokens", "", "tamaño del contexto y tokens gastados en esta sesión"],
|
|
169
|
+
["/new", "", "empezar una sesión nueva (historial vacío)"],
|
|
170
|
+
["/sessions", "", "listar sesiones guardadas"],
|
|
171
|
+
["/resume", "<id>", "reanudar una sesión guardada"],
|
|
172
|
+
["/flags", "", "flags de arranque: lampson --web --agent --yolo --workspace …"],
|
|
173
|
+
["/update", "", "actualizar Lampson a la última versión (git pull); también: lampson --update"],
|
|
174
|
+
["!<comando>", "", "correr un comando VOS en un terminal real (prompts y REPLs funcionan; ^C corta); la salida queda en el contexto del agente"],
|
|
175
|
+
["/help", "", "esta ayuda"],
|
|
176
|
+
["/exit", "", "salir (la sesión queda guardada)"]
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
task pad(s, n)
|
|
180
|
+
let out be s
|
|
181
|
+
while length(out) < n
|
|
182
|
+
set out to out + " "
|
|
183
|
+
give out
|
|
184
|
+
|
|
185
|
+
task help()
|
|
186
|
+
print("")
|
|
187
|
+
print(" Escribí lo que querés hacer en el proyecto. El agente lee, busca, edita y corre comandos con tools")
|
|
188
|
+
print(" acotadas al workspace; cada paso queda a la vista. Los comandos empiezan con /:")
|
|
189
|
+
print("")
|
|
190
|
+
each c in COMMANDS
|
|
191
|
+
print(" " + pad(c[0] + " " + c[1], 38) + c[2])
|
|
192
|
+
print("")
|
|
193
|
+
|
|
194
|
+
task flags()
|
|
195
|
+
print("")
|
|
196
|
+
print(" Flags de arranque (desde cualquier carpeta; el directorio actual es el workspace):")
|
|
197
|
+
print("")
|
|
198
|
+
print(" lampson REPL por terminal sobre el proyecto actual")
|
|
199
|
+
print(" lampson --web interfaz web en http://127.0.0.1:8080")
|
|
200
|
+
print(" lampson --workspace C:\\ruta elegir el proyecto sin hacer cd")
|
|
201
|
+
print(" lampson --agent plan perfil inicial: build | plan | review | explore")
|
|
202
|
+
print(" lampson --yolo | --strict | --ask permisos (--dangerously-skip-permissions = --yolo)")
|
|
203
|
+
print(" lampson --help ayuda del launcher")
|
|
204
|
+
print("")
|
|
205
|
+
print(" Configuración persistente (opcional): lampson\\.env → LAMPSON_PROVIDER, LAMPSON_API_KEY, LAMPSON_MODEL…")
|
|
206
|
+
print("")
|
|
207
|
+
|
|
208
|
+
task show_config(cfg, ws, profile, mode, opts)
|
|
209
|
+
print("")
|
|
210
|
+
print(" proveedor " + cfg["provider"] + " (" + cfg["wire"] + " → " + cfg["base_url"] + ")")
|
|
211
|
+
print(" modelo " + cfg["model"] + " max_tokens " + text(cfg["max_tokens"]) + " timeout " + text(floor(cfg["timeout"])) + "s")
|
|
212
|
+
print(" agente " + profile + " tools: " + join(agents.profile(profile)["tools"], ", "))
|
|
213
|
+
print(" permisos " + mode)
|
|
214
|
+
print(" límites " + text(opts["max_steps"]) + " pasos/turno · " + text(floor(opts["budget_tokens"])) + " tokens/turno · compacta a " + text(floor(opts["compact_at_tokens"])))
|
|
215
|
+
print(" workspace " + ws)
|
|
216
|
+
print("")
|
|
217
|
+
|
|
218
|
+
-- sugerencias para un comando desconocido: por prefijo
|
|
219
|
+
task suggest(input)
|
|
220
|
+
let word be split(input, " ")[0]
|
|
221
|
+
let hits be []
|
|
222
|
+
each c in COMMANDS
|
|
223
|
+
when starts_with(c[0], word)
|
|
224
|
+
set hits to append(hits, c[0])
|
|
225
|
+
when length(hits) == 0
|
|
226
|
+
give "comando desconocido: " + word + " · escribí / para ver la lista"
|
|
227
|
+
give "¿quisiste decir " + join(hits, " · ") + "?"
|
|
228
|
+
|
|
229
|
+
task banner(ws, cfg, profile, mode, sid)
|
|
230
|
+
print("")
|
|
231
|
+
print(" ██ █████ ███ ███ ██████ ███████ ██████ ███ ██")
|
|
232
|
+
print(" ██ ██ ██ ████████ ██ ██ ██ ██ ██ ████ ██")
|
|
233
|
+
print(" ██ ███████ ██ ██ ██ ██████ ███████ ██ ██ ██ ██ ██")
|
|
234
|
+
print(" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████")
|
|
235
|
+
print(" ██████ ██ ██ ██ ██ ███████ ██████ ██ ███")
|
|
236
|
+
print(" harness de agente en Synsema")
|
|
237
|
+
print(LINE)
|
|
238
|
+
print(" workspace " + ws)
|
|
239
|
+
print(" agente " + profile + " permisos " + mode + " modelo " + cfg["model"])
|
|
240
|
+
print(" sesión " + sid + " " + git.summary())
|
|
241
|
+
let upd be update.line()
|
|
242
|
+
when upd != ""
|
|
243
|
+
print(" " + yellow("⬆ " + upd) + dim(" (o /update acá)"))
|
|
244
|
+
print(LINE)
|
|
245
|
+
print(tree.render(0, 22))
|
|
246
|
+
print(LINE)
|
|
247
|
+
print(" Escribí un pedido, " + cyan("/") + " para los comandos, " + cyan("!") + "comando para correr algo vos.")
|
|
248
|
+
|
|
249
|
+
task skills_list()
|
|
250
|
+
let idx be skills.index()
|
|
251
|
+
let out be []
|
|
252
|
+
each n in sort_by(keys(idx), (x) => x)
|
|
253
|
+
set out to append(out, " " + n + " (" + idx[n]["source"] + "): " + idx[n]["description"])
|
|
254
|
+
when length(out) == 0
|
|
255
|
+
give " (sin skills)"
|
|
256
|
+
give join(out, "\n")
|
|
257
|
+
|
|
258
|
+
-- ---------- onboarding ----------
|
|
259
|
+
-- Primera vez (ningún proveedor con key) o /setup: elegir proveedor, modelo y pegar la key. Va a
|
|
260
|
+
-- lampson/.lampson/config.json (local, gitignored) y vale para terminal y web. Sin TTY no pregunta.
|
|
261
|
+
task setup_wizard()
|
|
262
|
+
print("")
|
|
263
|
+
print(" " + cyan("Configuremos el modelo.") + " Elegí un proveedor (número o nombre):")
|
|
264
|
+
let list be provider.providers()
|
|
265
|
+
each e in enumerate(list)
|
|
266
|
+
let p be e["item"]
|
|
267
|
+
print(" " + pad(text(e["index"] + 1) + ".", 4) + pad(p["name"], 12) + dim(p["model"]) + (when p["name"] == "ollama" then dim(" (local, sin key)") otherwise (when p["has_key"] then green(" ● key guardada") otherwise "")))
|
|
268
|
+
let choice be read_line(" proveedor: ")
|
|
269
|
+
when choice == nothing or trim(choice) == ""
|
|
270
|
+
print(" (sin cambios)")
|
|
271
|
+
give false
|
|
272
|
+
let name be lower(trim(choice))
|
|
273
|
+
when matches(name, "[0-9]+")
|
|
274
|
+
let i be floor(number(name)) - 1
|
|
275
|
+
when i >= 0 and i < length(list)
|
|
276
|
+
set name to list[i]["name"]
|
|
277
|
+
when not contains(provider.PRESETS, name)
|
|
278
|
+
print(" " + red("no conozco el proveedor " + name))
|
|
279
|
+
give false
|
|
280
|
+
let preset be provider.PRESETS[name]
|
|
281
|
+
let model be read_line(" modelo [" + preset["model"] + "]: ")
|
|
282
|
+
set model to when model == nothing then "" otherwise trim(model)
|
|
283
|
+
when name != "ollama"
|
|
284
|
+
print(" " + dim("la key se guarda en lampson/.lampson/config.json (solo en esta máquina); Enter para dejar la actual"))
|
|
285
|
+
let key be read_line(" API key de " + name + ": ")
|
|
286
|
+
when key != nothing and trim(key) != ""
|
|
287
|
+
settings.set_key(name, trim(key))
|
|
288
|
+
settings.set_default(name, model)
|
|
289
|
+
print(" " + green("listo") + " · " + name + " · " + (when model == "" then preset["model"] otherwise model) + dim(" (cambialo cuando quieras con /setup o /provider)"))
|
|
290
|
+
give true
|
|
291
|
+
|
|
292
|
+
-- ---------- imágenes en el REPL ----------
|
|
293
|
+
-- Una terminal no puede pegar una imagen, así que se lee del portapapeles (/paste) o de un archivo
|
|
294
|
+
-- (/image ruta) con un script del sistema que devuelve "media_type|w|h|base64". Quedan pendientes
|
|
295
|
+
-- hasta el próximo mensaje al agente (mismo formato canónico que la web: m["images"]).
|
|
296
|
+
let pending_images be []
|
|
297
|
+
|
|
298
|
+
task grab_image(mode, path)
|
|
299
|
+
let r be nothing
|
|
300
|
+
when env("OS", "") == "Windows_NT"
|
|
301
|
+
set r to run("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "lib/tools/img.ps1", mode, path], 60)
|
|
302
|
+
otherwise
|
|
303
|
+
set r to run("bash", ["lib/tools/img.sh", mode, path], 60)
|
|
304
|
+
let line be trim(replace_text(r["stdout"], "\r", ""))
|
|
305
|
+
when line == "" or starts_with(line, "ERROR|")
|
|
306
|
+
give {"error": when line == "" then trim(r["stderr"]) otherwise slice(line, 6, length(line))}
|
|
307
|
+
let parts be split(line, "|")
|
|
308
|
+
when length(parts) < 4
|
|
309
|
+
give {"error": "salida inesperada del lector de imágenes"}
|
|
310
|
+
give {"media_type": parts[0], "w": floor(number(parts[1])), "h": floor(number(parts[2])), "data": parts[3]}
|
|
311
|
+
|
|
312
|
+
task attach_image(mode, path)
|
|
313
|
+
when length(pending_images) >= 4
|
|
314
|
+
print(" " + yellow("máximo 4 imágenes por mensaje (/image para quitarlas)"))
|
|
315
|
+
give false
|
|
316
|
+
let im be grab_image(mode, path)
|
|
317
|
+
when contains(im, "error")
|
|
318
|
+
print(" " + red("✗ " + im["error"]))
|
|
319
|
+
give false
|
|
320
|
+
set pending_images to append(pending_images, im)
|
|
321
|
+
let vis be provider.supports_vision(cfg)
|
|
322
|
+
print(" " + green("📎 imagen " + text(length(pending_images)) + " adjunta") + dim(" · " + text(im["w"]) + "x" + text(im["h"]) + " · " + text(floor(length(im["data"]) * 3 / 4 / 1024)) + " KB · se manda con tu próximo mensaje"))
|
|
323
|
+
when not vis
|
|
324
|
+
print(" " + yellow("ojo: " + cfg["provider"] + " · " + cfg["model"] + " no acepta imágenes; el modelo recibirá solo una nota. Cambiá con /provider a uno con visión (anthropic, gpt-4o…)"))
|
|
325
|
+
give true
|
|
326
|
+
|
|
327
|
+
-- ---------- boot ----------
|
|
328
|
+
|
|
329
|
+
let cfg be provider.config()
|
|
330
|
+
when not provider.configured() and cfg["provider"] != "ollama"
|
|
331
|
+
print("")
|
|
332
|
+
print(" Bienvenido a Lampson. Todavía no hay ninguna API key configurada.")
|
|
333
|
+
when setup_wizard()
|
|
334
|
+
set cfg to provider.config()
|
|
335
|
+
let env_info be agents.env_info(cfg)
|
|
336
|
+
let profile be lower(env("LAMPSON_AGENT", "build"))
|
|
337
|
+
task system_for(p)
|
|
338
|
+
give {"role": "system", "content": prompt.build(env_info, agents.addendum_for(p))}
|
|
339
|
+
task opts_for(p, mode)
|
|
340
|
+
let o be loop.default_opts(agents.registry_for(p), agents.catalog_for(p), ask_user)
|
|
341
|
+
set o to loop.with_steps(o, agents.steps_for(p))
|
|
342
|
+
set o to loop.with_inbox(o, agents.parent_inbox, "")
|
|
343
|
+
give loop.with_mode(o, mode)
|
|
344
|
+
|
|
345
|
+
-- Subagente en background: `agent Child` (lib/agents.syn) corre en un hilo con intérprete propio y solo
|
|
346
|
+
-- puede llamar tasks TOP-LEVEL de este programa — esta es su única puerta de entrada al harness.
|
|
347
|
+
task lampson_subagent(spec_json)
|
|
348
|
+
give agents.run_child_json(spec_json)
|
|
349
|
+
|
|
350
|
+
-- !comando del usuario en un pseudo-terminal: la salida se ve EN VIVO y, si el proceso se queda esperando
|
|
351
|
+
-- (prompt y/N, contraseña, REPL, `npm init`…), lo que escribas se le manda como teclas + Enter.
|
|
352
|
+
-- Enter vacío = seguir esperando · ^C = cortar. Devuelve la salida (sin ANSI) para el contexto del agente.
|
|
353
|
+
task shell_argv(cmd)
|
|
354
|
+
when env("OS", "") == "Windows_NT"
|
|
355
|
+
give ["powershell", ["-NoLogo", "-NoProfile", "-Command", cmd]]
|
|
356
|
+
give ["bash", ["-lc", cmd]]
|
|
357
|
+
|
|
358
|
+
task run_interactive(cmd)
|
|
359
|
+
let sh be shell_argv(cmd)
|
|
360
|
+
let p be proc_spawn(sh[0], sh[1], {"cwd": env("LAMPSON_WORKSPACE", "workspace"), "pty": true, "cols": 120, "rows": 40})
|
|
361
|
+
let captured be []
|
|
362
|
+
let partial be ""
|
|
363
|
+
let alive be true
|
|
364
|
+
while alive
|
|
365
|
+
let ev be proc_recv(p, 5)
|
|
366
|
+
when ev == nothing
|
|
367
|
+
when proc_status(p) != "running"
|
|
368
|
+
set alive to false
|
|
369
|
+
otherwise
|
|
370
|
+
-- silencio: si quedó una línea a medias es casi seguro un prompt; dejar escribir
|
|
371
|
+
let hint be when trim(partial) == "" then "(sin salida hace 5 s · Enter: seguir esperando · texto: mandárselo · ^C: cortar) " otherwise partial + " "
|
|
372
|
+
let line be read_line(dim(hint))
|
|
373
|
+
set partial to ""
|
|
374
|
+
when line == nothing or trim(line) == "^C"
|
|
375
|
+
proc_kill(p)
|
|
376
|
+
set captured to append(captured, "[cortado por el usuario]")
|
|
377
|
+
otherwise when trim(line) != ""
|
|
378
|
+
proc_send(p, line + "\r")
|
|
379
|
+
otherwise when ev["type"] == "exit"
|
|
380
|
+
when partial != ""
|
|
381
|
+
print(" " + partial)
|
|
382
|
+
set captured to append(captured, partial)
|
|
383
|
+
set captured to append(captured, "[exit " + text(ev["data"]["exit_code"]) + "]")
|
|
384
|
+
set alive to false
|
|
385
|
+
otherwise
|
|
386
|
+
let raw be ev["data"]
|
|
387
|
+
when is_bytes(raw)
|
|
388
|
+
set raw to decode(raw, "utf8_lossy")
|
|
389
|
+
let chunk be strip_ansi(raw)
|
|
390
|
+
let lines be split(partial + chunk, "\n")
|
|
391
|
+
let n be length(lines)
|
|
392
|
+
each e in enumerate(lines)
|
|
393
|
+
when e["index"] < n - 1
|
|
394
|
+
print(" " + e["item"])
|
|
395
|
+
set captured to append(captured, e["item"])
|
|
396
|
+
set partial to lines[n - 1]
|
|
397
|
+
flush()
|
|
398
|
+
proc_close(p)
|
|
399
|
+
give join(captured, "\n")
|
|
400
|
+
|
|
401
|
+
let system_msg be system_for(profile)
|
|
402
|
+
-- servers MCP (globales en .lampson/mcp.json, del proyecto en workspace/.lampson/mcp.json): arrancan antes
|
|
403
|
+
-- de armar el catálogo de tools; hasta 8 s de espera a que estén listos
|
|
404
|
+
let mcp_servers be mcp.start_all(8)
|
|
405
|
+
each ms in mcp.summary()
|
|
406
|
+
print(" " + (when ms["status"] == "ready" then green("● mcp " + ms["name"]) otherwise red("○ mcp " + ms["name"] + " " + ms["status"])) + dim(" (" + ms["scope"] + ") " + text(length(ms["tools"])) + " tools" + (when ms["error"] != nothing then " · " + text(ms["error"]) otherwise "")))
|
|
407
|
+
-- language servers (lib/lsp.syn): solo se anuncian; arrancan lazy en la primera consulta
|
|
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
|
|
411
|
+
each l in lamps.summary()
|
|
412
|
+
print(" " + (when l["enabled"] then green("● lamp " + l["name"]) otherwise dim("○ lamp " + l["name"] + " off")) + dim(" (" + l["scope"] + ", " + l["kind"] + ") " + text(length(l["tools"])) + " tools" + (when l["error"] != nothing then " · " + text(l["error"]) otherwise "")))
|
|
413
|
+
let opts be opts_for(profile, lower(env("LAMPSON_PERMISSION", "ask")))
|
|
414
|
+
|
|
415
|
+
-- marca de corrida (blackboard): session.save la estampa en meta.run; reanudar una sesión guardada por
|
|
416
|
+
-- OTRA corrida dispara la nota de procesos muertos (proc.resume_note) para que el modelo no los crea vivos
|
|
417
|
+
share {"id": text(now())} as "lampson:run"
|
|
418
|
+
share {"kind": "terminal"} as "lampson:ui"
|
|
419
|
+
|
|
420
|
+
let sid be env("LAMPSON_SESSION", "")
|
|
421
|
+
let messages be [system_msg]
|
|
422
|
+
when sid != ""
|
|
423
|
+
when session.exists(sid)
|
|
424
|
+
let doc be session.load(sid)
|
|
425
|
+
set messages to doc["messages"]
|
|
426
|
+
print("reanudando sesión " + sid + " (" + text(length(messages)) + " mensajes)")
|
|
427
|
+
when session.from_other_run(doc)
|
|
428
|
+
let note be proc.resume_note(sid)
|
|
429
|
+
when note != ""
|
|
430
|
+
set messages to append(messages, {"role": "user", "content": note})
|
|
431
|
+
print(" " + cyan("✉ " + first_line(note, 140)))
|
|
432
|
+
otherwise
|
|
433
|
+
print("sesión " + sid + " no existe; creando una nueva")
|
|
434
|
+
set sid to ""
|
|
435
|
+
when sid == ""
|
|
436
|
+
set sid to session.new_id()
|
|
437
|
+
-- sesión actual (blackboard): la tool todo guarda su lista por sesión y no recibe contexto (lib/tools/todo.syn)
|
|
438
|
+
share {"id": sid} as "lampson:session"
|
|
439
|
+
|
|
440
|
+
banner(env_info["cwd"], cfg, profile, opts["permission_mode"], sid)
|
|
441
|
+
flush()
|
|
442
|
+
|
|
443
|
+
let total_usage be {"input": 0, "output": 0}
|
|
444
|
+
let mode be opts["permission_mode"]
|
|
445
|
+
let running be true
|
|
446
|
+
-- turnos automáticos por avisos de subagentes en background (padre idle): tope 3 seguidos, se
|
|
447
|
+
-- reinicia con input humano real (anti bucle auto-excitado, deepseek-harness)
|
|
448
|
+
let followups be 0
|
|
449
|
+
while running
|
|
450
|
+
let inbox be when followups < 3 then agents.parent_inbox(0, "") otherwise nothing
|
|
451
|
+
let line be nothing
|
|
452
|
+
when inbox != nothing
|
|
453
|
+
set followups to followups + 1
|
|
454
|
+
each m in inbox["messages"]
|
|
455
|
+
print("")
|
|
456
|
+
print(" " + cyan("✉ " + first_line(m, 140)))
|
|
457
|
+
set messages to append(messages, {"role": "user", "content": m})
|
|
458
|
+
trace.user(sid, "(turno automático por aviso de subagente)", profile, mode, cfg["model"])
|
|
459
|
+
set opts to opts_for(profile, mode)
|
|
460
|
+
let result be loop.run_turn(cfg, messages, opts, on_event)
|
|
461
|
+
trace.turn_end(sid, result)
|
|
462
|
+
set messages to result["messages"]
|
|
463
|
+
set total_usage to {"input": total_usage["input"] + result["usage"]["input"], "output": total_usage["output"] + result["usage"]["output"]}
|
|
464
|
+
print(dim(" ─── " + text(result["steps"]) + " pasos · " + fmt_tokens(result["usage"]["input"] + result["usage"]["output"]) + " tokens · turno automático por subagente"))
|
|
465
|
+
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
466
|
+
flush()
|
|
467
|
+
otherwise
|
|
468
|
+
set line to read_line("\n" + (when length(pending_images) > 0 then dim("📎" + text(length(pending_images)) + " ") otherwise "") + cyan("❯ "))
|
|
469
|
+
when inbox != nothing
|
|
470
|
+
set running to running
|
|
471
|
+
otherwise when line == nothing
|
|
472
|
+
set running to false
|
|
473
|
+
otherwise
|
|
474
|
+
set followups to 0
|
|
475
|
+
let input be trim(line)
|
|
476
|
+
when input == ""
|
|
477
|
+
set running to running
|
|
478
|
+
otherwise when input == "/exit" or input == "/quit"
|
|
479
|
+
set running to false
|
|
480
|
+
otherwise when input == "/help" or input == "/"
|
|
481
|
+
help()
|
|
482
|
+
otherwise when input == "/flags"
|
|
483
|
+
flags()
|
|
484
|
+
otherwise when input == "/update"
|
|
485
|
+
let u be update.check()
|
|
486
|
+
when not u["available"]
|
|
487
|
+
print(" ya estás en la última versión (" + u["current"] + ")")
|
|
488
|
+
otherwise
|
|
489
|
+
each n in u["notes"]
|
|
490
|
+
print(" " + dim("· " + n))
|
|
491
|
+
print(" " + update.apply())
|
|
492
|
+
otherwise when input == "/config"
|
|
493
|
+
show_config(cfg, env_info["cwd"], profile, mode, opts)
|
|
494
|
+
otherwise when starts_with(input, "/trace")
|
|
495
|
+
let n be trim(slice(input, 6, length(input)))
|
|
496
|
+
let lines be when n == "" then 40 otherwise floor(number(n))
|
|
497
|
+
print(dim(" " + trace.file_of(sid)))
|
|
498
|
+
print(trace.tail(sid, lines))
|
|
499
|
+
otherwise when starts_with(input, "/model")
|
|
500
|
+
let want be trim(slice(input, 6, length(input)))
|
|
501
|
+
when want == ""
|
|
502
|
+
print("modelo actual: " + cfg["model"] + " · uso: /model <nombre>")
|
|
503
|
+
let lm be provider.list_models(cfg)
|
|
504
|
+
when lm["error"] != nothing
|
|
505
|
+
print(" " + dim("no pude listar los modelos de " + cfg["provider"] + ": " + lm["error"]))
|
|
506
|
+
otherwise
|
|
507
|
+
print(" disponibles en " + cfg["provider"] + ": " + join(lm["models"], ", "))
|
|
508
|
+
otherwise
|
|
509
|
+
set cfg["model"] to provider.normalize_model(cfg["provider"], want)
|
|
510
|
+
print("modelo=" + want + " (solo esta sesión; para fijarlo: LAMPSON_MODEL en lampson\\.env)")
|
|
511
|
+
otherwise when starts_with(input, "/provider")
|
|
512
|
+
let args be where(split(trim(slice(input, 9, length(input))), " "), (x) => x != "")
|
|
513
|
+
when length(args) == 0
|
|
514
|
+
print(" proveedor actual: " + cfg["provider"] + " · modelo " + cfg["model"])
|
|
515
|
+
each p in provider.providers()
|
|
516
|
+
print(" " + (when p["has_key"] then green("● ") otherwise dim("○ ")) + pad(p["name"], 12) + dim(p["model"] + (when p["has_key"] then "" otherwise " (sin key: LAMPSON_API_KEY_" + upper(p["name"]) + " en lampson\\.env)")))
|
|
517
|
+
print(" uso: /provider <nombre> [modelo]")
|
|
518
|
+
otherwise when not contains(provider.PRESETS, lower(args[0]))
|
|
519
|
+
print(" no conozco el proveedor " + args[0] + " · disponibles: " + join(sort_by(keys(provider.PRESETS), (x) => x), ", "))
|
|
520
|
+
otherwise
|
|
521
|
+
set cfg to provider.config_for(args[0], when length(args) > 1 then args[1] otherwise "")
|
|
522
|
+
when not cfg["has_key"]
|
|
523
|
+
print(" " + yellow("ojo: no hay key para " + cfg["provider"] + " — agregá LAMPSON_API_KEY_" + upper(cfg["provider"]) + "=… en lampson\\.env (se usará LAMPSON_API_KEY, que probablemente falle)"))
|
|
524
|
+
print(" proveedor=" + cfg["provider"] + " · modelo=" + cfg["model"] + " · " + cfg["wire"] + " → " + cfg["base_url"] + dim(" (solo esta sesión; para fijarlo: LAMPSON_PROVIDER en .env)"))
|
|
525
|
+
otherwise when input == "/setup"
|
|
526
|
+
when setup_wizard()
|
|
527
|
+
set cfg to provider.config()
|
|
528
|
+
otherwise when input == "/paste"
|
|
529
|
+
attach_image("clip", "")
|
|
530
|
+
otherwise when starts_with(input, "/image")
|
|
531
|
+
let p be trim(slice(input, 6, length(input)))
|
|
532
|
+
when p == ""
|
|
533
|
+
when length(pending_images) == 0
|
|
534
|
+
print(" sin imágenes pendientes · uso: /image <ruta> o /paste")
|
|
535
|
+
otherwise
|
|
536
|
+
print(" " + text(length(pending_images)) + " imagen(es) pendiente(s) — se mandan con tu próximo mensaje. ¿Quitarlas? (s/N)")
|
|
537
|
+
let a be read_line(" ")
|
|
538
|
+
when a != nothing and lower(trim(a)) == "s"
|
|
539
|
+
set pending_images to []
|
|
540
|
+
print(" quitadas")
|
|
541
|
+
otherwise
|
|
542
|
+
attach_image("file", p)
|
|
543
|
+
otherwise when starts_with(input, "/delete")
|
|
544
|
+
let id be trim(slice(input, 7, length(input)))
|
|
545
|
+
when id == ""
|
|
546
|
+
print(" uso: /delete <id> (ver /sessions)")
|
|
547
|
+
otherwise when id == sid
|
|
548
|
+
print(" esa es la sesión actual; hacé /new primero")
|
|
549
|
+
otherwise
|
|
550
|
+
print(" " + session.delete(id))
|
|
551
|
+
otherwise when input == "/new"
|
|
552
|
+
set sid to session.new_id()
|
|
553
|
+
set messages to [system_msg]
|
|
554
|
+
share {"id": sid} as "lampson:session"
|
|
555
|
+
print("nueva sesión " + sid)
|
|
556
|
+
otherwise when input == "/sessions"
|
|
557
|
+
each s in session.list()
|
|
558
|
+
print(" " + s["id"] + " " + s["title"])
|
|
559
|
+
otherwise when starts_with(input, "/resume ")
|
|
560
|
+
let id be trim(slice(input, 8, length(input)))
|
|
561
|
+
when session.exists(id)
|
|
562
|
+
set sid to id
|
|
563
|
+
let rdoc be session.load(id)
|
|
564
|
+
set messages to rdoc["messages"]
|
|
565
|
+
share {"id": sid} as "lampson:session"
|
|
566
|
+
print("reanudada " + id + " (" + text(length(messages)) + " mensajes)")
|
|
567
|
+
when session.from_other_run(rdoc)
|
|
568
|
+
let note be proc.resume_note(sid)
|
|
569
|
+
when note != ""
|
|
570
|
+
set messages to append(messages, {"role": "user", "content": note})
|
|
571
|
+
print(" " + cyan("✉ " + first_line(note, 140)))
|
|
572
|
+
otherwise
|
|
573
|
+
print("no existe " + id)
|
|
574
|
+
otherwise when input == "/files"
|
|
575
|
+
print(tree.render(2, 200))
|
|
576
|
+
otherwise when input == "/procs"
|
|
577
|
+
let items be proc.list()
|
|
578
|
+
when length(items) == 0
|
|
579
|
+
print(" (ningún proceso gestionado)")
|
|
580
|
+
each p in items
|
|
581
|
+
print(" " + (when p["running"] then green("● running") otherwise red("○ exited ")) + " " + p["name"] + " $ " + p["command"])
|
|
582
|
+
let ports be proc.listeners()
|
|
583
|
+
when length(ports) > 0
|
|
584
|
+
print("")
|
|
585
|
+
print(" " + dim("puertos en escucha en esta máquina (de cualquier programa, no solo de lampson; /kill <pid> para matar uno):"))
|
|
586
|
+
each l in ports
|
|
587
|
+
print(" " + yellow(":" + text(l["port"])) + " pid " + text(l["pid"]) + " " + l["name"] + " " + dim(first_line(l["command"], 110)))
|
|
588
|
+
otherwise when starts_with(input, "/logs")
|
|
589
|
+
let args be split(trim(slice(input, 5, length(input))), " ")
|
|
590
|
+
when args[0] == ""
|
|
591
|
+
print(" uso: /logs <nombre> [líneas]")
|
|
592
|
+
otherwise
|
|
593
|
+
let n be when length(args) > 1 then number(args[1]) otherwise 60
|
|
594
|
+
print(proc.logs(args[0], n))
|
|
595
|
+
otherwise when starts_with(input, "/kill")
|
|
596
|
+
let pid be trim(slice(input, 5, length(input)))
|
|
597
|
+
print(" " + (when pid == "" then "uso: /kill <pid> (ver /procs)" otherwise proc.kill_pid(number(pid))))
|
|
598
|
+
otherwise when starts_with(input, "/stop")
|
|
599
|
+
let nm be trim(slice(input, 5, length(input)))
|
|
600
|
+
print(" " + (when nm == "" then "uso: /stop <nombre>" otherwise proc.halt(nm)))
|
|
601
|
+
otherwise when starts_with(input, "/memory")
|
|
602
|
+
let nm be trim(slice(input, 7, length(input)))
|
|
603
|
+
when nm == ""
|
|
604
|
+
let items be memo.list()
|
|
605
|
+
print(" " + dim("memory/" + memo.slug() + "/"))
|
|
606
|
+
when length(items) == 0
|
|
607
|
+
print(" (sin notas todavía — el agente las crea con la tool memory)")
|
|
608
|
+
each it in items
|
|
609
|
+
print(" " + yellow(it["name"]) + " " + it["title"])
|
|
610
|
+
otherwise
|
|
611
|
+
print(memo.note_read(nm))
|
|
612
|
+
otherwise when input == "/todo"
|
|
613
|
+
let items be todo.load(sid)
|
|
614
|
+
print(when length(items) == 0 then " sin tareas (el agente las crea con la tool todo)" otherwise " " + replace_text(todo.format(items), "\n", "\n "))
|
|
615
|
+
otherwise when input == "/lsp" or starts_with(input, "/lsp ")
|
|
616
|
+
let srest be trim(slice(input, 4, length(input)))
|
|
617
|
+
when starts_with(srest, "add ")
|
|
618
|
+
-- /lsp add typescript [--project] · /lsp add mylang cmd args… --ext .x=lang [--ext .y=lang2]
|
|
619
|
+
let sscope be when contains(srest, "--project") then "project" otherwise "global"
|
|
620
|
+
let sline be trim(replace_text(srest, "--project", ""))
|
|
621
|
+
let stoks be where(split(slice(sline, 4, length(sline)), " "), (x) => x != "")
|
|
622
|
+
when length(stoks) == 0
|
|
623
|
+
print(" uso: /lsp add <" + join(keys(lsp.PRESETS), "|") + "> [--project] · /lsp add <nombre> <comando…> --ext .ext=languageId")
|
|
624
|
+
otherwise
|
|
625
|
+
let langs be {}
|
|
626
|
+
let cmdtoks be []
|
|
627
|
+
let ti be 1
|
|
628
|
+
while ti < length(stoks)
|
|
629
|
+
when stoks[ti] == "--ext" and ti + 1 < length(stoks)
|
|
630
|
+
let kv be split(stoks[ti + 1], "=")
|
|
631
|
+
when length(kv) == 2
|
|
632
|
+
set langs[kv[0]] to kv[1]
|
|
633
|
+
set ti to ti + 2
|
|
634
|
+
otherwise
|
|
635
|
+
set cmdtoks to append(cmdtoks, stoks[ti])
|
|
636
|
+
set ti to ti + 1
|
|
637
|
+
try
|
|
638
|
+
print(" " + lsp.add_server(stoks[0], when length(cmdtoks) == 0 then nothing otherwise join(cmdtoks, " "), when length(keys(langs)) == 0 then nothing otherwise langs, sscope))
|
|
639
|
+
recover err
|
|
640
|
+
print(" " + red(text(err)))
|
|
641
|
+
otherwise when starts_with(srest, "remove ")
|
|
642
|
+
try
|
|
643
|
+
print(" " + lsp.remove_server(trim(slice(srest, 7, length(srest)))))
|
|
644
|
+
recover err
|
|
645
|
+
print(" " + red(text(err)))
|
|
646
|
+
otherwise
|
|
647
|
+
let ss be lsp.summary()
|
|
648
|
+
when length(ss) == 0
|
|
649
|
+
print(" sin language servers configurados. Agregá uno: " + cyan("/lsp add typescript") + dim(" (presets: " + join(keys(lsp.PRESETS), ", ") + ")"))
|
|
650
|
+
print(" " + dim("el agente los usa con la tool lsp: symbols (estructura de un archivo sin leerlo), definition, references, hover"))
|
|
651
|
+
each s in ss
|
|
652
|
+
print(" " + (when s["status"] == "ready" then green("● ") otherwise (when s["status"] == "error" or s["status"] == "exited" then red("○ ") otherwise dim("○ "))) + pad(s["name"], 12) + dim(pad(s["scope"], 8) + pad(s["status"], 8) + join(s["extensions"], " ") + " $ " + s["command"]) + (when s["error"] != nothing then red(" " + text(s["error"])) otherwise ""))
|
|
653
|
+
print(" " + dim("idle = arranca en la primera consulta del agente"))
|
|
654
|
+
otherwise when input == "/lamps" or starts_with(input, "/lamps ")
|
|
655
|
+
let lrest be trim(slice(input, 6, length(input)))
|
|
656
|
+
when starts_with(lrest, "run ")
|
|
657
|
+
-- /lamps run <lámpara> <tool> [json de args] → la corre el usuario, sin el modelo
|
|
658
|
+
let rt be where(split(trim(slice(lrest, 4, length(lrest))), " "), (x) => x != "")
|
|
659
|
+
when length(rt) < 2
|
|
660
|
+
print(" uso: /lamps run <lámpara> <tool> [{\"arg\": \"valor\"}]")
|
|
661
|
+
otherwise
|
|
662
|
+
let rargs be {}
|
|
663
|
+
when length(rt) > 2
|
|
664
|
+
try
|
|
665
|
+
set rargs to json_decode(join(slice(rt, 2, length(rt)), " "))
|
|
666
|
+
recover err
|
|
667
|
+
print(" " + red("args: JSON inválido"))
|
|
668
|
+
try
|
|
669
|
+
print(lamps.call(lamps.tool_name(rt[0], rt[1]), rargs))
|
|
670
|
+
recover err
|
|
671
|
+
print(" " + red(text(err)))
|
|
672
|
+
otherwise when starts_with(lrest, "remove ")
|
|
673
|
+
try
|
|
674
|
+
print(" " + lamps.remove(trim(slice(lrest, 7, length(lrest)))))
|
|
675
|
+
recover err
|
|
676
|
+
print(" " + red(text(err)))
|
|
677
|
+
set opts to opts_for(profile, mode)
|
|
678
|
+
otherwise when starts_with(lrest, "on ") or starts_with(lrest, "off ")
|
|
679
|
+
let lon be starts_with(lrest, "on ")
|
|
680
|
+
try
|
|
681
|
+
print(" " + lamps.set_enabled(trim(slice(lrest, when lon then 3 otherwise 4, length(lrest))), lon))
|
|
682
|
+
recover err
|
|
683
|
+
print(" " + red(text(err)))
|
|
684
|
+
set opts to opts_for(profile, mode)
|
|
685
|
+
otherwise
|
|
686
|
+
let ls be lamps.summary()
|
|
687
|
+
when length(ls) == 0
|
|
688
|
+
print(" sin lámparas. Una lámpara es una carpeta con lamp.json: " + dim("lampson\\lamps\\<nombre>\\ (global) · .lampson\\lamps\\<nombre>\\ del repo (proyecto)"))
|
|
689
|
+
print(" " + dim("el agente también puede crearlas (write en .lampson/lamps/) y pedirte encenderlas"))
|
|
690
|
+
each l in ls
|
|
691
|
+
print(" " + (when l["enabled"] then green("● ") otherwise dim("○ ")) + pad(l["name"], 14) + dim(pad(l["scope"], 8) + pad(l["kind"], 5)) + (when l["error"] != nothing then red("rota: " + text(l["error"])) otherwise dim(text(length(l["tools"])) + " tools [" + join(l["tools"], ", ") + "]" + (when l["kind"] == "syn" then " techo: " + l["caps"] otherwise " $ " + l["command"]))))
|
|
692
|
+
print(" " + dim("encender/apagar: /lamps on <nombre> · /lamps off <nombre> · correr una vos: /lamps run <lámpara> <tool> [json]"))
|
|
693
|
+
otherwise when input == "/mcp" or starts_with(input, "/mcp ")
|
|
694
|
+
let mrest be trim(slice(input, 4, length(input)))
|
|
695
|
+
when starts_with(mrest, "add ")
|
|
696
|
+
-- /mcp add <nombre> <comando…> [--project] (env/keys: editá el mcp.json a mano, no van por terminal)
|
|
697
|
+
let mscope be when contains(mrest, "--project") then "project" otherwise "global"
|
|
698
|
+
let mline be trim(replace_text(mrest, "--project", ""))
|
|
699
|
+
let mtoks be where(split(slice(mline, 4, length(mline)), " "), (x) => x != "")
|
|
700
|
+
when length(mtoks) < 2
|
|
701
|
+
print(" uso: /mcp add <nombre> <comando…> [--project] p.ej.: /mcp add github npx -y @modelcontextprotocol/server-github")
|
|
702
|
+
otherwise
|
|
703
|
+
try
|
|
704
|
+
print(" " + mcp.add_server(mtoks[0], join(slice(mtoks, 1, length(mtoks)), " "), {}, mscope))
|
|
705
|
+
recover err
|
|
706
|
+
print(" " + red(text(err)))
|
|
707
|
+
set opts to opts_for(profile, mode)
|
|
708
|
+
otherwise when starts_with(mrest, "remove ")
|
|
709
|
+
try
|
|
710
|
+
print(" " + mcp.remove_server(trim(slice(mrest, 7, length(mrest)))))
|
|
711
|
+
recover err
|
|
712
|
+
print(" " + red(text(err)))
|
|
713
|
+
set opts to opts_for(profile, mode)
|
|
714
|
+
otherwise
|
|
715
|
+
let sm be mcp.summary()
|
|
716
|
+
when length(sm) == 0
|
|
717
|
+
print(" sin servers MCP conectados (no hay ninguno configurado).")
|
|
718
|
+
print(" conectá uno: " + cyan("/mcp add <nombre> <comando…> [--project]") + dim(" · o editá lampson\\.lampson\\mcp.json (global) / .lampson\\mcp.json del repo (proyecto)"))
|
|
719
|
+
print(" " + dim("el agente también puede: pedile \"conectá el MCP de github\" y te va a pedir aprobación"))
|
|
720
|
+
each ms in sm
|
|
721
|
+
print(" " + (when ms["status"] == "ready" then green("● ") otherwise red("○ ")) + pad(ms["name"], 14) + dim(pad(ms["scope"], 8) + ms["status"] + " $ " + ms["command"]) + (when ms["error"] != nothing then red(" " + text(ms["error"])) otherwise ""))
|
|
722
|
+
when length(ms["tools"]) > 0
|
|
723
|
+
print(" " + dim(join(ms["tools"], ", ")))
|
|
724
|
+
-- servers que se pusieron listos después del arranque: recomponer el catálogo
|
|
725
|
+
set opts to opts_for(profile, mode)
|
|
726
|
+
otherwise when input == "/agents"
|
|
727
|
+
agents.prune(10, 1800)
|
|
728
|
+
let kids be agents.list_children()
|
|
729
|
+
when length(kids) == 0
|
|
730
|
+
print(" sin subagentes todavía (el agente los lanza con la tool delegate)")
|
|
731
|
+
each c in kids
|
|
732
|
+
print(" " + (when c["status"] == "running" then green("● running") otherwise dim("○ " + c["status"] + " ")) + " " + c["id"] + " " + text(c["steps"]) + " pasos " + dim(first_line(c["brief"], 80)) + dim(" log: " + agents.DIR + "/" + c["id"] + ".log"))
|
|
733
|
+
otherwise when input == "/skills"
|
|
734
|
+
print(skills_list())
|
|
735
|
+
print(dim(" instalar una de skills.sh: pedile al agente «instalá la skill X de owner/repo» (global, pide aprobación) · o !npx skills add owner/repo --skill X -g"))
|
|
736
|
+
otherwise when input == "/tokens"
|
|
737
|
+
print("contexto≈" + text(loop.estimate_tokens(messages)) + " tokens · gastado in=" + text(total_usage["input"]) + " out=" + text(total_usage["output"]))
|
|
738
|
+
otherwise when input == "/yolo" or input == "/ask" or input == "/strict"
|
|
739
|
+
set mode to slice(input, 1, length(input))
|
|
740
|
+
set opts to opts_for(profile, mode)
|
|
741
|
+
print("permisos=" + opts["permission_mode"])
|
|
742
|
+
otherwise when starts_with(input, "/agent")
|
|
743
|
+
let want be trim(slice(input, 6, length(input)))
|
|
744
|
+
when want == ""
|
|
745
|
+
print("agente=" + profile + " · disponibles: " + join(agents.names(), ", "))
|
|
746
|
+
otherwise when contains(agents.PROFILES, want)
|
|
747
|
+
set profile to want
|
|
748
|
+
set system_msg to system_for(profile)
|
|
749
|
+
set opts to opts_for(profile, mode)
|
|
750
|
+
-- el system prompt cambia con el perfil; el historial se conserva (cambiar de agente no borra la conversación)
|
|
751
|
+
set messages to [system_msg] + slice(messages, 1, length(messages))
|
|
752
|
+
print("agente=" + profile + " · tools: " + join(agents.profile(profile)["tools"], ", "))
|
|
753
|
+
otherwise
|
|
754
|
+
print("no existe el agente " + want + " · disponibles: " + join(agents.names(), ", "))
|
|
755
|
+
otherwise when starts_with(input, "/")
|
|
756
|
+
print(" " + suggest(input))
|
|
757
|
+
otherwise when starts_with(input, "!")
|
|
758
|
+
-- comando del USUARIO: corre sin política de permisos (lo escribiste vos), salida al contexto
|
|
759
|
+
let cmd be trim(slice(input, 1, length(input)))
|
|
760
|
+
when cmd == ""
|
|
761
|
+
print(" uso: !<comando> p.ej. !git status · !npm test · !cat .env")
|
|
762
|
+
otherwise
|
|
763
|
+
print(" " + yellow("$ " + cmd))
|
|
764
|
+
flush()
|
|
765
|
+
let out be run_interactive(cmd)
|
|
766
|
+
set messages to append(messages, {"role": "user", "content": "[The user ran this command in the shell themselves]\n$ " + cmd + "\n" + tools.truncate(out, 6000)})
|
|
767
|
+
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
768
|
+
otherwise
|
|
769
|
+
when length(pending_images) > 0
|
|
770
|
+
set messages to append(messages, {"role": "user", "content": input, "images": pending_images})
|
|
771
|
+
set pending_images to []
|
|
772
|
+
otherwise
|
|
773
|
+
set messages to append(messages, {"role": "user", "content": input})
|
|
774
|
+
trace.user(sid, input, profile, mode, cfg["model"])
|
|
775
|
+
-- catálogo fresco por turno: si el turno anterior conectó/quitó un server MCP, sus tools
|
|
776
|
+
-- entran/salen acá (con los mismos servers el catálogo es idéntico → el prompt cache no se corta)
|
|
777
|
+
set opts to opts_for(profile, mode)
|
|
778
|
+
let result be loop.run_turn(cfg, messages, opts, on_event)
|
|
779
|
+
trace.turn_end(sid, result)
|
|
780
|
+
set messages to result["messages"]
|
|
781
|
+
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"
|
|
783
|
+
when result["stopped"] == "max_steps"
|
|
784
|
+
set summary to summary + " · " + red("límite de " + text(opts["max_steps"]) + " pasos por turno")
|
|
785
|
+
print(dim(" ─── " + summary))
|
|
786
|
+
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
|
+
otherwise when result["stopped"] != "done"
|
|
788
|
+
set summary to summary + " · " + red("detenido: " + result["stopped"])
|
|
789
|
+
print(dim(" ─── " + summary))
|
|
790
|
+
otherwise
|
|
791
|
+
print(dim(" ─── " + summary))
|
|
792
|
+
session.save(sid, messages, {"title": session.title_of(messages)})
|
|
793
|
+
flush()
|
|
794
|
+
-- los procesos gestionados (servidores) y los subagentes en background mueren con lampson
|
|
795
|
+
agents.stop_all()
|
|
796
|
+
mcp.stop_all()
|
|
797
|
+
lsp.stop_all()
|
|
798
|
+
proc.halt_all()
|
|
799
|
+
print("bye · sesión " + sid)
|