lampson 0.2.8 → 0.2.9
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 +10 -1
- package/README.md +8 -3
- package/bin/lampson.js +1 -1
- package/chat.syn +111 -7
- package/cli.syn +143 -7
- package/edge.syn +26 -0
- package/hub.tpl.syn +127 -6
- package/lampson.ps1 +43 -2
- package/lampson.sh +40 -2
- package/lib/agents.syn +27 -6
- package/lib/approvals.syn +2 -2
- package/lib/auth.syn +274 -0
- package/lib/computer.syn +2000 -0
- package/lib/computer_judge.syn +410 -0
- package/lib/fs.syn +19 -3
- package/lib/git.syn +2 -2
- package/lib/judge.syn +78 -0
- package/lib/line.syn +2 -2
- package/lib/loop.syn +36 -4
- package/lib/lsp.syn +4 -4
- package/lib/mcp.syn +20 -4
- package/lib/md.syn +11 -11
- package/lib/origin.syn +67 -0
- package/lib/permission.syn +21 -2
- package/lib/prompt.syn +9 -1
- package/lib/provider.syn +70 -20
- package/lib/schedule.syn +12 -12
- package/lib/session.syn +5 -5
- package/lib/settings.syn +65 -1
- package/lib/skills.syn +22 -6
- package/lib/tools/bash.syn +1 -1
- package/lib/tools/common.syn +1 -1
- package/lib/tools/fetch.syn +98 -71
- package/lib/tools/grep.syn +1 -1
- package/lib/tools/memo.syn +3 -3
- package/lib/tools/proc.syn +38 -20
- package/lib/tools/qr.js +26 -0
- package/lib/tools/url.syn +5 -5
- package/lib/tools.syn +23 -2
- package/lib/trace.syn +2 -2
- package/lib/tree.syn +29 -13
- package/lib/update.syn +1 -1
- package/lib/workspaces.syn +202 -36
- package/package.json +3 -2
- package/public/apple-touch-icon.png +0 -0
- package/public/badge-96.png +0 -0
- package/public/css/code.css +48 -0
- package/public/css/hub.css +9 -0
- package/public/css/login.css +16 -0
- package/public/css/panel.css +30 -0
- package/public/css/tokens.css +1 -1
- package/public/hub.html +11 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/icons-src/badge.svg +12 -0
- package/public/icons-src/icon-maskable.svg +15 -0
- package/public/icons-src/icon.svg +13 -0
- package/public/index.html +14 -3
- package/public/js/agents.js +1 -1
- package/public/js/chat.js +1 -1
- package/public/js/config.js +145 -2
- package/public/js/core.js +4 -2
- package/public/js/devices.js +80 -0
- package/public/js/editor.js +232 -0
- package/public/js/login.js +78 -0
- package/public/js/procs.js +1 -1
- package/public/js/pwa.js +5 -0
- package/public/js/tree.js +12 -4
- package/public/login.html +43 -0
- package/public/manifest.webmanifest +16 -0
- package/public/offline.html +24 -0
- package/public/sw.js +63 -0
- package/public/vendor/highlight.min.js +1343 -0
- package/public/vendor/qrcode.js +2297 -0
- package/skills/computer-use/SKILL.md +178 -0
- package/skills/lampson/SKILL.md +5 -0
- package/web.syn +67 -6
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
-- lib/computer_judge.syn — «jev-use» para computer_use: un modelo System One (Jev de TypeSafe, o Laya local) elige
|
|
2
|
+
-- la próxima acción de una lista CERRADA de candidatos, en vez de que el LLM lea el árbol entero y elija un índice
|
|
3
|
+
--
|
|
4
|
+
-- De dónde sale (2026-09-28): la receta jev-use de trycua/cua (libs/cua-driver/examples/jev-use, RFC 4268). Lo que
|
|
5
|
+
-- se copió porque allá está medido:
|
|
6
|
+
-- * el modelo nunca inventa: elige UN id de una tabla que arma el harness; el id se resuelve a la acción ya
|
|
7
|
+
-- armada (elemento + snapshot, o ref de página). El texto a tipear sale del LLM (`text`), nunca del juez.
|
|
8
|
+
-- * MEDIDO ACÁ (2026-09-28, Calculadora de Windows en español, Laya local = checkpoint `english`): el backend
|
|
9
|
+
-- local elige por el ID de la opción, no por su descripción ("press 7" con ids b0…b29 → b7; idéntico con
|
|
10
|
+
-- etiquetas en inglés o en español). Por eso el id ES legible: «button Siete». Y con la interfaz en español el
|
|
11
|
+
-- checkpoint inglés se equivoca con confianza 100% («presionar 7» → «Tres»): la compuerta no protege ahí; el
|
|
12
|
+
-- objetivo tiene que venir con las palabras de la pantalla y el LLM verifica siempre.
|
|
13
|
+
-- * candidatos: solo controles con clase conocida (button, checkbox, radio, popup, menu_item, link, tab,
|
|
14
|
+
-- text_input, toggle, option), habilitados, con tamaño, con etiqueta (y etiqueta ≠ value: así el contenido
|
|
15
|
+
-- tipeado nunca termina en un id), fuera de la barra de título. Ids ESTABLES (clase + etiqueta), nunca índices.
|
|
16
|
+
-- Tope 32 + los reservados `reobserve` y `abstain` (Jev eligió bien 220/220 con 3-7 candidatos; con más de
|
|
17
|
+
-- 12 no está medido — por eso, pasado el tope, se ordena por parecido con el objetivo antes de cortar). El
|
|
18
|
+
-- 24 de cua viene del límite de 26 letras de Cua-S1; una Calculadora ya trae 29 controles útiles.
|
|
19
|
+
-- * riesgo por palabra entera (borrar, enviar, comprar, cerrar): esos controles NO se ofrecen — el LLM los hace
|
|
20
|
+
-- a mano con element=N / ref, y pasan por la aprobación normal. Es una guarda contra accidentes, no autorización.
|
|
21
|
+
-- * el modelo NO decide si la tarea terminó (Jev contesta «listo» en el paso 0): eso lo verifica el LLM.
|
|
22
|
+
-- * cua no pone umbral (lo deja al que llama): acá hay compuerta de probabilidad Y de margen sobre el segundo.
|
|
23
|
+
--
|
|
24
|
+
-- Puro: sin driver ni juez; lo usa lib/computer.syn (acción `act`, y page_select cuando el texto no coincide).
|
|
25
|
+
|
|
26
|
+
-- clase → acción. Roles de Windows UIA, macOS AX (sin el prefijo AX), Linux AT-SPI y ARIA (página), normalizados
|
|
27
|
+
-- (minúsculas, solo letras y dígitos). UIA "Text" es texto estático (AT-SPI "text" es editable, pero en Linux la
|
|
28
|
+
-- ruta nativa todavía no se probó): no entra.
|
|
29
|
+
export let ROLE_CLASS be {
|
|
30
|
+
"button": "button", "splitbutton": "button", "pushbutton": "button", "menubutton": "popup",
|
|
31
|
+
"checkbox": "checkbox", "menuitemcheckbox": "checkbox",
|
|
32
|
+
"radiobutton": "radio", "radio": "radio", "menuitemradio": "radio",
|
|
33
|
+
"switch": "toggle", "togglebutton": "toggle",
|
|
34
|
+
"combobox": "popup", "popupbutton": "popup", "listbox": "popup",
|
|
35
|
+
"menuitem": "menu_item", "menubaritem": "menu_item",
|
|
36
|
+
"hyperlink": "link", "link": "link",
|
|
37
|
+
"tabitem": "tab", "tab": "tab", "pagetab": "tab",
|
|
38
|
+
"edit": "text_input", "textbox": "text_input", "searchbox": "text_input", "textfield": "text_input", "textarea": "text_input", "searchfield": "text_input", "entry": "text_input", "spinbutton": "text_input",
|
|
39
|
+
"option": "option", "listitem": "option", "treeitem": "option"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export let CLASS_ACTION be {"button": "press", "checkbox": "toggle", "radio": "select", "toggle": "toggle", "popup": "open_menu", "menu_item": "press", "link": "press", "tab": "press", "text_input": "set_text", "option": "select"}
|
|
43
|
+
|
|
44
|
+
-- palabra entera, sin distinguir mayúsculas; en inglés y en español (las etiquetas vienen en el idioma del usuario)
|
|
45
|
+
export let RISK_WORDS be {
|
|
46
|
+
"destructive": ["delete", "remove", "erase", "trash", "discard", "clear all", "format", "reset", "eliminar", "borrar", "quitar", "vaciar", "descartar", "restablecer", "formatear", "papelera"],
|
|
47
|
+
"send": ["send", "submit", "post", "publish", "share", "reply", "enviar", "publicar", "compartir", "responder"],
|
|
48
|
+
"purchase": ["buy", "purchase", "pay", "checkout", "order", "subscribe", "comprar", "pagar", "suscribirse", "suscribir", "pedido", "finalizar compra"],
|
|
49
|
+
"close_unsaved": ["close", "quit", "exit", "don't save", "discard changes", "cerrar", "salir", "no guardar", "descartar cambios"]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export let MAX_CANDIDATES be 32
|
|
53
|
+
export let MAX_HISTORY be 16
|
|
54
|
+
export let MAX_ELEMENTS_IN_STATE be 64
|
|
55
|
+
-- compuerta: probabilidad del elegido y margen sobre el segundo (cua documenta que el llamador debe ponerlas)
|
|
56
|
+
export let MIN_P be 0.6
|
|
57
|
+
export let MIN_MARGIN be 0.15
|
|
58
|
+
export let MAX_STEPS be 6
|
|
59
|
+
|
|
60
|
+
export let RESERVED be {
|
|
61
|
+
"reobserve": "Take no action and obtain a fresh observation of the window, because the current observation looks stale, incomplete, or contradicts the goal's progress.",
|
|
62
|
+
"abstain": "Stop without acting if none of the proposed actions is safe or moves toward the goal."
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export let INSTRUCTION be "Which single action should run next to move toward the goal?"
|
|
66
|
+
|
|
67
|
+
-- "AXButton" | "Button" | "push button" → "button"
|
|
68
|
+
export task norm_role(r)
|
|
69
|
+
let low be regex_replace(lower(text(r)), "[^a-z0-9]", "")
|
|
70
|
+
when starts_with(low, "ax") and length(low) > 2
|
|
71
|
+
set low to slice(low, 2, length(low))
|
|
72
|
+
give low
|
|
73
|
+
|
|
74
|
+
export task role_class(r)
|
|
75
|
+
let n be norm_role(r)
|
|
76
|
+
when contains(ROLE_CLASS, n)
|
|
77
|
+
give ROLE_CLASS[n]
|
|
78
|
+
give nothing
|
|
79
|
+
|
|
80
|
+
task squash(s)
|
|
81
|
+
give trim(regex_replace(replace(text(s), "’", "'"), "\\s+", " "))
|
|
82
|
+
|
|
83
|
+
-- categoría de riesgo de una etiqueta, o nothing (palabra entera: «Resetear» no es «reset», «Guardar» no es «guardar como»)
|
|
84
|
+
export task risk_of(label)
|
|
85
|
+
let low be " " + regex_replace(lower(squash(label)), "[^a-z0-9áéíóúñü' ]", " ") + " "
|
|
86
|
+
each cat in keys(RISK_WORDS)
|
|
87
|
+
each w in RISK_WORDS[cat]
|
|
88
|
+
when contains(low, " " + w + " ")
|
|
89
|
+
give cat
|
|
90
|
+
give nothing
|
|
91
|
+
|
|
92
|
+
-- id legible de un candidato: «button Guardar», «text_input Nombre de archivo» (el modelo lee el id: ver arriba)
|
|
93
|
+
export task readable_id(cls, label)
|
|
94
|
+
let l be squash(label)
|
|
95
|
+
when length(l) > 48
|
|
96
|
+
set l to trim(slice(l, 0, 48))
|
|
97
|
+
give cls + " " + l
|
|
98
|
+
|
|
99
|
+
-- slug ASCII de 32 como máximo; sin nada ASCII → 8 hex del sha256
|
|
100
|
+
let TRANSLIT be {"á": "a", "à": "a", "ä": "a", "â": "a", "ã": "a", "é": "e", "è": "e", "ë": "e", "ê": "e", "í": "i", "ì": "i", "ï": "i", "î": "i", "ó": "o", "ò": "o", "ö": "o", "ô": "o", "õ": "o", "ú": "u", "ù": "u", "ü": "u", "û": "u", "ñ": "n", "ç": "c"}
|
|
101
|
+
|
|
102
|
+
task ascii_fold(s)
|
|
103
|
+
let out be lower(text(s))
|
|
104
|
+
each k in keys(TRANSLIT)
|
|
105
|
+
set out to replace(out, k, TRANSLIT[k])
|
|
106
|
+
give out
|
|
107
|
+
|
|
108
|
+
export task slug(label)
|
|
109
|
+
let s be regex_replace(ascii_fold(squash(label)), "[^a-z0-9]+", "-")
|
|
110
|
+
set s to regex_replace(s, "^-+|-+$", "")
|
|
111
|
+
when length(s) > 32
|
|
112
|
+
set s to regex_replace(slice(s, 0, 32), "-+$", "")
|
|
113
|
+
when s == ""
|
|
114
|
+
give slice(hex(sha256(text(label))), 2, 10)
|
|
115
|
+
give s
|
|
116
|
+
|
|
117
|
+
task short(s, n)
|
|
118
|
+
let t be squash(s)
|
|
119
|
+
give when length(t) > n then slice(t, 0, n) + "…" otherwise t
|
|
120
|
+
|
|
121
|
+
-- descripción que lee el modelo (las de cua, literales donde se pudo)
|
|
122
|
+
export task describe_candidate(cls, label, state, text_value)
|
|
123
|
+
-- comillas a mano: json_encode escapa lo no ASCII (ú) y el modelo leería «Menú»
|
|
124
|
+
let q be "\"" + replace(short(label, 60), "\"", "'") + "\""
|
|
125
|
+
when cls == "text_input"
|
|
126
|
+
let st be when state == "" then "" otherwise " The field currently reports " + state + "."
|
|
127
|
+
give "Set the text field " + q + " to the text the task supplies" + (when text_value != nothing then " (\"" + replace(short(text_value, 40), "\"", "'") + "\")" otherwise "") + ", replacing its contents." + st
|
|
128
|
+
when cls == "checkbox" or cls == "toggle"
|
|
129
|
+
give "Toggle the " + (when cls == "checkbox" then "checkbox " otherwise "switch ") + q + "." + (when state != "" then " It is currently " + state + "." otherwise "")
|
|
130
|
+
when cls == "radio"
|
|
131
|
+
give "Select the radio option " + q + "." + (when state != "" then " It is currently " + state + "." otherwise "")
|
|
132
|
+
when cls == "popup"
|
|
133
|
+
give "Open the pop-up menu " + q + ". Its options become candidates on the next observation."
|
|
134
|
+
when cls == "option"
|
|
135
|
+
give "Select the option " + q + "." + (when state != "" then " It is currently " + state + "." otherwise "")
|
|
136
|
+
when cls == "menu_item"
|
|
137
|
+
give "Press the menu item labeled " + q + "."
|
|
138
|
+
when cls == "tab"
|
|
139
|
+
give "Switch to the tab labeled " + q + "."
|
|
140
|
+
when cls == "link"
|
|
141
|
+
give "Follow the link labeled " + q + "."
|
|
142
|
+
give "Press the button labeled " + q + "."
|
|
143
|
+
|
|
144
|
+
task num_or(m, k, d)
|
|
145
|
+
when not contains(m, k)
|
|
146
|
+
give d
|
|
147
|
+
when m[k] == nothing
|
|
148
|
+
give d
|
|
149
|
+
try
|
|
150
|
+
give number(text(m[k]))
|
|
151
|
+
recover err
|
|
152
|
+
give d
|
|
153
|
+
|
|
154
|
+
task txt(m, k)
|
|
155
|
+
when not contains(m, k)
|
|
156
|
+
give ""
|
|
157
|
+
when m[k] == nothing
|
|
158
|
+
give ""
|
|
159
|
+
give text(m[k])
|
|
160
|
+
|
|
161
|
+
-- estado sin values (nunca viaja el contenido de un campo): empty / has_text / contains_required_value …
|
|
162
|
+
task native_state(e, cls, want_text)
|
|
163
|
+
when cls == "text_input"
|
|
164
|
+
let v be txt(e, "value")
|
|
165
|
+
when trim(v) == ""
|
|
166
|
+
give "empty"
|
|
167
|
+
when want_text != nothing and trim(v) == trim(text(want_text))
|
|
168
|
+
give "contains_required_value"
|
|
169
|
+
give "has_text"
|
|
170
|
+
when cls == "checkbox" or cls == "toggle"
|
|
171
|
+
when contains(e, "selected")
|
|
172
|
+
give when e["selected"] == true then "checked" otherwise "unchecked"
|
|
173
|
+
let v be lower(txt(e, "value"))
|
|
174
|
+
when v == "on" or v == "true" or v == "1" or v == "checked"
|
|
175
|
+
give "checked"
|
|
176
|
+
when v == "off" or v == "false" or v == "0" or v == "unchecked"
|
|
177
|
+
give "unchecked"
|
|
178
|
+
give ""
|
|
179
|
+
when cls == "radio" or cls == "option"
|
|
180
|
+
when contains(e, "selected")
|
|
181
|
+
give when e["selected"] == true then "selected" otherwise "not selected"
|
|
182
|
+
give ""
|
|
183
|
+
give ""
|
|
184
|
+
|
|
185
|
+
-- ¿cuelga de la barra de título? (Minimizar/Maximizar/Cerrar/menú de sistema no son candidatos)
|
|
186
|
+
task in_title_bar(e, by_index)
|
|
187
|
+
let cur be e
|
|
188
|
+
let hops be 0
|
|
189
|
+
while cur != nothing and hops < 40
|
|
190
|
+
when norm_role(txt(cur, "role")) == "titlebar"
|
|
191
|
+
give true
|
|
192
|
+
let pi be num_or(cur, "parent_index", -1)
|
|
193
|
+
set cur to when pi >= 0 and contains(by_index, text(floor(pi))) then by_index[text(floor(pi))] otherwise nothing
|
|
194
|
+
set hops to hops + 1
|
|
195
|
+
give false
|
|
196
|
+
|
|
197
|
+
-- botones de ventana que no cuelgan de un TitleBar (apps UWP en ApplicationFrameHost: «Minimizar Calculadora»)
|
|
198
|
+
let CHROME_WORDS be ["minimize", "maximize", "restore", "minimizar", "maximizar", "restaurar"]
|
|
199
|
+
|
|
200
|
+
task is_chrome_label(label)
|
|
201
|
+
let first be lower(split(squash(label) + " ", " ")[0])
|
|
202
|
+
give contains(CHROME_WORDS, first)
|
|
203
|
+
|
|
204
|
+
-- palabras del objetivo (3+ letras) que aparecen en la etiqueta: para ordenar cuando hay más candidatos que el tope
|
|
205
|
+
task goal_words(goal)
|
|
206
|
+
let out be []
|
|
207
|
+
each w in split(regex_replace(lower(text(goal)), "[^a-z0-9áéíóúñü]+", " "), " ")
|
|
208
|
+
when length(w) >= 3 and not contains(out, w)
|
|
209
|
+
set out to append(out, w)
|
|
210
|
+
give out
|
|
211
|
+
|
|
212
|
+
task overlap(words, label)
|
|
213
|
+
let low be lower(text(label))
|
|
214
|
+
let n be 0
|
|
215
|
+
each w in words
|
|
216
|
+
when contains(low, w)
|
|
217
|
+
set n to n + 1
|
|
218
|
+
give n
|
|
219
|
+
|
|
220
|
+
-- palabra clave del objetivo para filtrar un snapshot (query): la más larga que no sea un verbo de acción ni un
|
|
221
|
+
-- sustantivo genérico de interfaz. «aplicar el filtro Con control de temperatura» → «temperatura»; «completar el
|
|
222
|
+
-- correo» → «correo». nothing si no queda ninguna.
|
|
223
|
+
let QUERY_STOP be ["completar", "presionar", "escribir", "buscar", "aplicar", "seleccionar", "elegir", "marcar", "abrir", "cerrar", "hacer", "click", "clic", "clickear", "responder", "poner", "ingresar", "llenar", "rellenar", "tocar", "ir", "press", "type", "fill", "open", "select", "choose", "enter", "search", "apply", "tick", "check", "click", "write", "set", "the", "field", "campo", "botón", "boton", "button", "filtro", "filter", "opción", "opcion", "option", "link", "enlace", "menu", "menú", "para", "con", "del", "los", "las", "una", "uno", "que", "sobre", "with", "into", "from", "that", "this"]
|
|
224
|
+
|
|
225
|
+
export task goal_query(goal)
|
|
226
|
+
let best be nothing
|
|
227
|
+
each w in split(regex_replace(lower(text(goal)), "[^a-z0-9áéíóúñü@._-]+", " "), " ")
|
|
228
|
+
when length(w) >= 4 and not contains(QUERY_STOP, w)
|
|
229
|
+
when best == nothing or length(w) > length(best)
|
|
230
|
+
set best to w
|
|
231
|
+
give best
|
|
232
|
+
|
|
233
|
+
-- ids únicos: el segundo con la misma base lleva « (2)» (estable entre observaciones iguales)
|
|
234
|
+
task unique_id(base, seen)
|
|
235
|
+
when not contains(seen, base)
|
|
236
|
+
give base
|
|
237
|
+
let k be 2
|
|
238
|
+
while contains(seen, base + " (" + text(k) + ")")
|
|
239
|
+
set k to k + 1
|
|
240
|
+
give base + " (" + text(k) + ")"
|
|
241
|
+
|
|
242
|
+
-- recorte al tope: si sobran, primero los que comparten palabras con el objetivo (orden estable dentro de cada grupo)
|
|
243
|
+
task cap_ranked(cands, goal)
|
|
244
|
+
when length(cands) <= MAX_CANDIDATES
|
|
245
|
+
give {"cands": cands, "dropped": 0}
|
|
246
|
+
let words be goal_words(goal)
|
|
247
|
+
let scored be []
|
|
248
|
+
let i be 0
|
|
249
|
+
each c in cands
|
|
250
|
+
set scored to append(scored, {"c": c, "s": overlap(words, c["label"]), "i": i})
|
|
251
|
+
set i to i + 1
|
|
252
|
+
let ranked be sort_by(scored, (x) => (0 - x["s"]) * 100000 + x["i"])
|
|
253
|
+
let out be []
|
|
254
|
+
each x in slice(ranked, 0, MAX_CANDIDATES)
|
|
255
|
+
set out to append(out, x["c"])
|
|
256
|
+
-- de vuelta al orden de pantalla: el modelo lee la lista como está en la ventana
|
|
257
|
+
give {"cands": sort_by(out, (c) => c["order"]), "dropped": length(cands) - MAX_CANDIDATES}
|
|
258
|
+
|
|
259
|
+
-- elementos de get_window_state → {cands: [{id, desc, cls, action, label, state, element}], excluded: {motivo: n},
|
|
260
|
+
-- risky: [etiquetas], dropped, elements: [{role_class, label, state}]}. `want_text` = el texto que el LLM pasó
|
|
261
|
+
-- (sin él no hay candidatos set_text).
|
|
262
|
+
export task native_candidates(elements, goal, want_text)
|
|
263
|
+
let by_index be {}
|
|
264
|
+
each e in elements
|
|
265
|
+
when contains(e, "element_index")
|
|
266
|
+
set by_index[text(floor(number(text(e["element_index"]))))] to e
|
|
267
|
+
let cands be []
|
|
268
|
+
let seen be []
|
|
269
|
+
let excluded be {}
|
|
270
|
+
let risky be []
|
|
271
|
+
let summary be []
|
|
272
|
+
let order be 0
|
|
273
|
+
each e in elements
|
|
274
|
+
let cls be role_class(txt(e, "role"))
|
|
275
|
+
let why be nothing
|
|
276
|
+
let label be squash(txt(e, "label"))
|
|
277
|
+
when cls == nothing
|
|
278
|
+
set why to "unknown_role"
|
|
279
|
+
otherwise when contains(e, "enabled") and e["enabled"] == false
|
|
280
|
+
set why to "disabled"
|
|
281
|
+
otherwise when not contains(e, "element_index")
|
|
282
|
+
set why to "no_index"
|
|
283
|
+
otherwise when label == ""
|
|
284
|
+
set why to "unlabelled"
|
|
285
|
+
otherwise when cls != "text_input" and label == squash(txt(e, "value"))
|
|
286
|
+
set why to "label_is_value"
|
|
287
|
+
otherwise when cls == "text_input" and trim(txt(e, "value")) != "" and label == squash(txt(e, "value"))
|
|
288
|
+
set why to "label_is_value"
|
|
289
|
+
otherwise when contains(e, "frame") and e["frame"] != nothing and (num_or(e["frame"], "w", 1) <= 0 or num_or(e["frame"], "h", 1) <= 0)
|
|
290
|
+
set why to "off_screen"
|
|
291
|
+
otherwise when in_title_bar(e, by_index) or is_chrome_label(label)
|
|
292
|
+
set why to "window_chrome"
|
|
293
|
+
otherwise when cls == "text_input" and want_text == nothing
|
|
294
|
+
set why to "no_text_supplied"
|
|
295
|
+
when why == nothing
|
|
296
|
+
let rk be risk_of(label)
|
|
297
|
+
when rk != nothing
|
|
298
|
+
set why to "risk_" + rk
|
|
299
|
+
set risky to append(risky, label)
|
|
300
|
+
when why != nothing
|
|
301
|
+
set excluded[why] to (when contains(excluded, why) then excluded[why] otherwise 0) + 1
|
|
302
|
+
otherwise
|
|
303
|
+
let state be native_state(e, cls, want_text)
|
|
304
|
+
let id be unique_id(readable_id(cls, label), seen)
|
|
305
|
+
set seen to append(seen, id)
|
|
306
|
+
set cands to append(cands, {"id": id, "desc": describe_candidate(cls, label, state, want_text), "cls": cls, "action": CLASS_ACTION[cls], "label": label, "state": state, "element": floor(number(text(e["element_index"]))), "order": order})
|
|
307
|
+
set order to order + 1
|
|
308
|
+
when length(summary) < MAX_ELEMENTS_IN_STATE
|
|
309
|
+
set summary to append(summary, {"role_class": cls, "label": short(label, 60), "state": state})
|
|
310
|
+
let capped be cap_ranked(cands, goal)
|
|
311
|
+
give {"cands": capped["cands"], "excluded": excluded, "risky": risky, "dropped": capped["dropped"], "elements": summary}
|
|
312
|
+
|
|
313
|
+
-- refs de un snapshot de página (semantic_v2: {ref, role, name, actions, visibility}) → la misma forma, con `ref`
|
|
314
|
+
export task page_candidates(refs, goal, want_text)
|
|
315
|
+
let cands be []
|
|
316
|
+
let seen be []
|
|
317
|
+
let excluded be {}
|
|
318
|
+
let risky be []
|
|
319
|
+
let summary be []
|
|
320
|
+
let order be 0
|
|
321
|
+
each r in refs
|
|
322
|
+
let cls be role_class(txt(r, "role"))
|
|
323
|
+
-- en la web un combobox suele ser un campo con autocompletado (el buscador de MercadoLibre): con texto para
|
|
324
|
+
-- escribir va como campo; describirlo como «abrir menú» bajó a Jev al 59% (2026-09-28)
|
|
325
|
+
when cls == "popup" and want_text != nothing and norm_role(txt(r, "role")) == "combobox"
|
|
326
|
+
set cls to "text_input"
|
|
327
|
+
let label be squash(txt(r, "name"))
|
|
328
|
+
let why be nothing
|
|
329
|
+
when cls == nothing
|
|
330
|
+
set why to "unknown_role"
|
|
331
|
+
otherwise when label == ""
|
|
332
|
+
set why to "unlabelled"
|
|
333
|
+
otherwise when txt(r, "ref") == ""
|
|
334
|
+
set why to "no_ref"
|
|
335
|
+
otherwise when cls == "text_input" and want_text == nothing
|
|
336
|
+
set why to "no_text_supplied"
|
|
337
|
+
otherwise when lower(txt(r, "visibility")) == "hidden"
|
|
338
|
+
set why to "hidden"
|
|
339
|
+
when why == nothing
|
|
340
|
+
let rk be risk_of(label)
|
|
341
|
+
when rk != nothing
|
|
342
|
+
set why to "risk_" + rk
|
|
343
|
+
set risky to append(risky, label)
|
|
344
|
+
when why != nothing
|
|
345
|
+
set excluded[why] to (when contains(excluded, why) then excluded[why] otherwise 0) + 1
|
|
346
|
+
otherwise
|
|
347
|
+
let id be unique_id(readable_id(cls, label), seen)
|
|
348
|
+
set seen to append(seen, id)
|
|
349
|
+
set cands to append(cands, {"id": id, "desc": describe_candidate(cls, label, "", want_text), "cls": cls, "action": CLASS_ACTION[cls], "label": label, "state": "", "ref": txt(r, "ref"), "order": order})
|
|
350
|
+
set order to order + 1
|
|
351
|
+
when length(summary) < MAX_ELEMENTS_IN_STATE
|
|
352
|
+
set summary to append(summary, {"role_class": cls, "label": short(label, 60), "state": ""})
|
|
353
|
+
let capped be cap_ranked(cands, goal)
|
|
354
|
+
give {"cands": capped["cands"], "excluded": excluded, "risky": risky, "dropped": capped["dropped"], "elements": summary}
|
|
355
|
+
|
|
356
|
+
-- criteria del `choose`: {id: descripción} con los reservados al final
|
|
357
|
+
export task criteria(cands)
|
|
358
|
+
let m be {}
|
|
359
|
+
each c in cands
|
|
360
|
+
set m[c["id"]] to c["desc"]
|
|
361
|
+
each k in keys(RESERVED)
|
|
362
|
+
set m[k] to RESERVED[k]
|
|
363
|
+
give m
|
|
364
|
+
|
|
365
|
+
-- estado que ve el juez: objetivo, dónde, historial acotado. Sin la lista de elementos: las opciones YA son los
|
|
366
|
+
-- controles, y Laya tiene 512 tokens de contexto (estado + pregunta + opciones) — lo que sobra se trunca por el final
|
|
367
|
+
export task decision_state(goal, where, elements, history)
|
|
368
|
+
let h be history
|
|
369
|
+
when length(h) > MAX_HISTORY
|
|
370
|
+
set h to slice(h, length(h) - MAX_HISTORY, length(h))
|
|
371
|
+
let st be {"goal": short(goal, 600), "where": where}
|
|
372
|
+
when length(h) > 0
|
|
373
|
+
set st["history"] to h
|
|
374
|
+
give st
|
|
375
|
+
|
|
376
|
+
-- compuerta sobre una respuesta `choose` del bloque judge →
|
|
377
|
+
-- {verdict: "act" | "reobserve" | "abstain" | "unsure" | "offline", id, p, margin, top: [{id, p}]}
|
|
378
|
+
export task gate(answer, min_p, min_margin)
|
|
379
|
+
when answer == nothing
|
|
380
|
+
give {"verdict": "offline", "id": nothing, "p": 0, "margin": 0, "top": []}
|
|
381
|
+
when not contains(answer, "available") or answer["available"] != true
|
|
382
|
+
give {"verdict": "offline", "id": nothing, "p": 0, "margin": 0, "top": []}
|
|
383
|
+
let probs be when contains(answer, "probabilities") then answer["probabilities"] otherwise {}
|
|
384
|
+
let pairs be []
|
|
385
|
+
each k in keys(probs)
|
|
386
|
+
set pairs to append(pairs, {"id": k, "p": number(text(probs[k]))})
|
|
387
|
+
let ranked be sort_by(pairs, (x) => 0 - x["p"])
|
|
388
|
+
let top be slice(ranked, 0, when length(ranked) < 4 then length(ranked) otherwise 4)
|
|
389
|
+
when length(ranked) == 0
|
|
390
|
+
give {"verdict": "unsure", "id": nothing, "p": 0, "margin": 0, "top": []}
|
|
391
|
+
let best be ranked[0]
|
|
392
|
+
let second be when length(ranked) > 1 then ranked[1]["p"] otherwise 0
|
|
393
|
+
let margin be best["p"] - second
|
|
394
|
+
-- `none` (la opción de escape de `or nothing`) o el reservado abstain: el juez no ve nada que sirva
|
|
395
|
+
when best["id"] == "none" or best["id"] == "abstain"
|
|
396
|
+
give {"verdict": "abstain", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
|
|
397
|
+
-- empate arriba: no es accionable (cua: «a tied top score is not actionable»)
|
|
398
|
+
when margin <= 0
|
|
399
|
+
give {"verdict": "unsure", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
|
|
400
|
+
when best["p"] < min_p or margin < min_margin
|
|
401
|
+
give {"verdict": "unsure", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
|
|
402
|
+
when best["id"] == "reobserve"
|
|
403
|
+
give {"verdict": "reobserve", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
|
|
404
|
+
give {"verdict": "act", "id": best["id"], "p": best["p"], "margin": margin, "top": top}
|
|
405
|
+
|
|
406
|
+
export task find(cands, id)
|
|
407
|
+
each c in cands
|
|
408
|
+
when c["id"] == id
|
|
409
|
+
give c
|
|
410
|
+
give nothing
|
package/lib/fs.syn
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
use "./tools/common.syn" as c
|
|
9
9
|
|
|
10
10
|
task sh_quote(s)
|
|
11
|
-
give "'" +
|
|
11
|
+
give "'" + replace(s, "'", "'\\''") + "'"
|
|
12
12
|
|
|
13
13
|
-- shell POSIX aunque LAMPSON_SHELL apunte a cmd: mv/cp/rm/mkdir son de Git Bash
|
|
14
14
|
task shell()
|
|
@@ -55,7 +55,7 @@ task check_name(name)
|
|
|
55
55
|
let n be trim(text(name))
|
|
56
56
|
when n == "" or n == "." or n == ".." or contains(n, "/") or contains(n, "\\")
|
|
57
57
|
raise("invalid name \"" + text(name) + "\": one path segment, without / or \\")
|
|
58
|
-
when length(
|
|
58
|
+
when length(regex_find_all(n, "[<>:\"|?*]")) > 0
|
|
59
59
|
raise("invalid name \"" + n + "\": < > : \" | ? * are not allowed in file names")
|
|
60
60
|
give n
|
|
61
61
|
|
|
@@ -102,6 +102,20 @@ export task create(dir, name, content)
|
|
|
102
102
|
write_file(c.ROOT + "/" + rel, when content == nothing then "" otherwise text(content))
|
|
103
103
|
give {"path": rel, "is_dir": false}
|
|
104
104
|
|
|
105
|
+
-- guardar desde el editor de la web: pisa SOLO un archivo que ya existe (crear es `create`), dentro del workspace.
|
|
106
|
+
-- "¿cambió en disco desde que lo abrí?" lo comprueba el cliente releyendo antes de guardar (js/editor.js), así el
|
|
107
|
+
-- servidor no guarda estado por archivo.
|
|
108
|
+
export task write(path, content)
|
|
109
|
+
require file("workspace")
|
|
110
|
+
require file("workspace/*")
|
|
111
|
+
let t be target(path)
|
|
112
|
+
must_exist(t)
|
|
113
|
+
when t["info"]["is_dir"]
|
|
114
|
+
raise("\"" + t["rel"] + "\" is a folder")
|
|
115
|
+
let s be when content == nothing then "" otherwise text(content)
|
|
116
|
+
write_file(t["real"], s)
|
|
117
|
+
give {"path": t["rel"], "is_dir": false, "lines": length(split(s, "\n"))}
|
|
118
|
+
|
|
105
119
|
export task rename(path, name)
|
|
106
120
|
require exec
|
|
107
121
|
require env("LAMPSON_*")
|
|
@@ -215,6 +229,8 @@ export task apply(b)
|
|
|
215
229
|
give mkdir(path, name)
|
|
216
230
|
when op == "create"
|
|
217
231
|
give create(path, name, when contains(b, "content") then b["content"] otherwise "")
|
|
232
|
+
when op == "write"
|
|
233
|
+
give write(path, when contains(b, "content") then b["content"] otherwise "")
|
|
218
234
|
when op == "rename"
|
|
219
235
|
give rename(path, name)
|
|
220
236
|
when op == "move"
|
|
@@ -223,4 +239,4 @@ export task apply(b)
|
|
|
223
239
|
give copy(path, dest)
|
|
224
240
|
when op == "delete"
|
|
225
241
|
give remove(path)
|
|
226
|
-
raise("unknown op '" + op + "' (mkdir, create, rename, move, copy, delete)")
|
|
242
|
+
raise("unknown op '" + op + "' (mkdir, create, write, rename, move, copy, delete)")
|
package/lib/git.syn
CHANGED
|
@@ -21,12 +21,12 @@ export task status()
|
|
|
21
21
|
let branch be ""
|
|
22
22
|
let changes be {}
|
|
23
23
|
let counts be {"modified": 0, "added": 0, "deleted": 0, "untracked": 0}
|
|
24
|
-
each line in split(
|
|
24
|
+
each line in split(replace(out, "\r", ""), "\n")
|
|
25
25
|
when starts_with(line, "## ")
|
|
26
26
|
set branch to split(split(slice(line, 3, length(line)), "...")[0], " ")[0]
|
|
27
27
|
otherwise when length(line) > 3
|
|
28
28
|
let xy be slice(line, 0, 2)
|
|
29
|
-
let path be
|
|
29
|
+
let path be replace(trim(slice(line, 3, length(line))), "\"", "")
|
|
30
30
|
when contains(path, " -> ")
|
|
31
31
|
set path to split(path, " -> ")[1]
|
|
32
32
|
let code be trim(xy)
|
package/lib/judge.syn
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
-- lib/judge.syn — el juez de decisión de Lampson: un modelo System One que contesta preguntas tipadas con
|
|
2
|
+
-- probabilidades (el bloque `judge` de Synsema: whether / choose / rate), no con texto
|
|
3
|
+
--
|
|
4
|
+
-- Es una credencial/configuración de Lampson, como el proveedor del LLM — no de una tool. Hoy lo usan:
|
|
5
|
+
-- * computer_use: la acción `act` (elige el próximo control de una lista cerrada) y page_select (elige la opción
|
|
6
|
+
-- de un desplegable cuando el texto no coincide) — lib/computer.syn + lib/computer_judge.syn
|
|
7
|
+
-- y cualquier parte de Lampson que necesite una decisión calibrada y barata puede usarlo con `require judge` y
|
|
8
|
+
-- judge.on() como compuerta (clasificar, rutear, validar, decidir si algo aplica…).
|
|
9
|
+
--
|
|
10
|
+
-- Backends (se elige en ⚙ → Juez de decisión, /judge en la terminal):
|
|
11
|
+
-- jev TypeSafe Jev, en la nube: rápido (~0,3 s por decisión), multilingüe, necesita API key (keys.typesafe)
|
|
12
|
+
-- laya Laya local (ModernBERT, ~840 MB en disco): sin red ni costo, ~1-2 s por decisión en CPU; medido
|
|
13
|
+
-- 2026-09-28: con interfaces en español el checkpoint `english` se equivoca con confianza alta
|
|
14
|
+
-- off sin juez
|
|
15
|
+
-- El runtime lee el proveedor del ENTORNO al arrancar el proceso (settings.judge_env): cambiarlo reinicia el
|
|
16
|
+
-- workspace (web) o pide salir y volver a entrar (terminal).
|
|
17
|
+
|
|
18
|
+
use "./settings.syn" as settings
|
|
19
|
+
|
|
20
|
+
export let MODES be ["off", "jev", "laya"]
|
|
21
|
+
|
|
22
|
+
export task norm_mode(v)
|
|
23
|
+
let t be lower(trim(text(when v == nothing then "" otherwise v)))
|
|
24
|
+
when t == "jev" or t == "typesafe"
|
|
25
|
+
give "jev"
|
|
26
|
+
when t == "laya" or t == "local"
|
|
27
|
+
give "laya"
|
|
28
|
+
give "off"
|
|
29
|
+
|
|
30
|
+
-- el modo configurado (config.json / .env), esté vivo o no en este proceso
|
|
31
|
+
export task mode()
|
|
32
|
+
require file.read(".lampson")
|
|
33
|
+
require file.read(".lampson/*")
|
|
34
|
+
require env("LAMPSON_*")
|
|
35
|
+
give norm_mode(settings.judge_setting()["mode"])
|
|
36
|
+
|
|
37
|
+
-- encendido en la configuración Y con un proveedor vivo en este proceso: la compuerta de quien lo use
|
|
38
|
+
export task on()
|
|
39
|
+
require file.read(".lampson")
|
|
40
|
+
require file.read(".lampson/*")
|
|
41
|
+
require env("LAMPSON_*")
|
|
42
|
+
when mode() == "off"
|
|
43
|
+
give false
|
|
44
|
+
give judge_available()
|
|
45
|
+
|
|
46
|
+
-- {mode, model_setting, has_key, live, provider_model, restart, from_env}: lo que ven la UI y /judge
|
|
47
|
+
export task status()
|
|
48
|
+
require file.read(".lampson")
|
|
49
|
+
require file.read(".lampson/*")
|
|
50
|
+
require env("LAMPSON_*")
|
|
51
|
+
let js be settings.judge_setting()
|
|
52
|
+
let m be norm_mode(js["mode"])
|
|
53
|
+
let live be judge_available()
|
|
54
|
+
let pm be judge_model()
|
|
55
|
+
-- el proceso corre otro proveedor que el configurado (o ninguno): hace falta reiniciar
|
|
56
|
+
let restart be false
|
|
57
|
+
when m != "off" and not live
|
|
58
|
+
set restart to true
|
|
59
|
+
when m == "laya" and pm != nothing and not starts_with(text(pm), "laya:")
|
|
60
|
+
set restart to true
|
|
61
|
+
when m == "jev" and pm != nothing and starts_with(text(pm), "laya:")
|
|
62
|
+
set restart to true
|
|
63
|
+
give {"mode": m, "model_setting": text(js["model"]), "has_key": settings.raw_key("typesafe") != "", "live": live, "provider_model": when pm == nothing then "" otherwise text(pm), "restart": restart, "from_env": js["from_env"]}
|
|
64
|
+
|
|
65
|
+
-- guardar el modo (y opcionalmente el modelo / la key de TypeSafe) → {ok, error}
|
|
66
|
+
export task configure(m, model, key)
|
|
67
|
+
require file(".lampson")
|
|
68
|
+
require file(".lampson/*")
|
|
69
|
+
require env("LAMPSON_*")
|
|
70
|
+
let nm be lower(trim(text(m)))
|
|
71
|
+
when not contains(MODES, nm) and nm != "typesafe" and nm != "local"
|
|
72
|
+
give {"ok": false, "error": "judge: off, jev or laya"}
|
|
73
|
+
set nm to norm_mode(nm)
|
|
74
|
+
when key != nothing
|
|
75
|
+
when trim(text(key)) != ""
|
|
76
|
+
settings.set_key("typesafe", trim(text(key)))
|
|
77
|
+
settings.set_judge(nm, model)
|
|
78
|
+
give {"ok": true, "error": nothing}
|
package/lib/line.syn
CHANGED
|
@@ -37,7 +37,7 @@ task starts(s, p)
|
|
|
37
37
|
task candidates(buf, ctx)
|
|
38
38
|
when not starts(buf, "/") or contains(buf, "\n")
|
|
39
39
|
give {"kind": "", "items": [], "token": ""}
|
|
40
|
-
let sp be
|
|
40
|
+
let sp be regex_capture(buf, "^(/\S*)\s+(.*)$")
|
|
41
41
|
when sp == nothing
|
|
42
42
|
-- comando a medias: recientes primero cuando es "/" pelado
|
|
43
43
|
let items be []
|
|
@@ -208,7 +208,7 @@ export task read(prompt, color, ctx)
|
|
|
208
208
|
set result to nothing
|
|
209
209
|
set done to true
|
|
210
210
|
otherwise when ev["type"] == "paste"
|
|
211
|
-
set st to insert(st,
|
|
211
|
+
set st to insert(st, replace(ev["text"], "\r", ""))
|
|
212
212
|
set st to refresh_menu(st, ctx)
|
|
213
213
|
otherwise when ev["type"] == "resize"
|
|
214
214
|
ui.set_cols(ev["cols"])
|