@runuai/host 0.9.9 → 0.9.11
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/images/standard/container/code-server-settings.json +38 -11
- package/images/standard/container/uai-init +33 -2
- package/lib/agent-cli.ts +30 -5
- package/lib/agents/durable-proc.ts +16 -2
- package/lib/agents/transport.ts +53 -2
- package/lib/orchestrator.ts +101 -21
- package/package.json +1 -1
- package/runner/runner.mjs +3 -3
- package/scripts/agent/task-up.sh +23 -7
- package/src/protocol.ts +19 -5
|
@@ -9,28 +9,55 @@
|
|
|
9
9
|
"workbench.welcomePage.walkthroughs.openOnInstall": false,
|
|
10
10
|
"window.commandCenter": false,
|
|
11
11
|
"chat.commandCenter.enabled": false,
|
|
12
|
+
"chat.viewSessions.enabled": false,
|
|
13
|
+
"chat.disableAIFeatures": true,
|
|
12
14
|
"update.mode": "none",
|
|
13
15
|
"extensions.autoCheckUpdates": false,
|
|
14
16
|
"extensions.autoUpdate": false,
|
|
15
17
|
"git.openRepositoryInParentFolders": "always",
|
|
16
|
-
"workbench.activityBar.location": "top",
|
|
17
|
-
"chat.viewSessions.enabled": false,
|
|
18
18
|
"workbench.secondarySideBar.defaultVisibility": "hidden",
|
|
19
|
+
|
|
20
|
+
"//4": "Colour theme. Overwritten at FIRST init by uai-init when the task",
|
|
21
|
+
"//5": "carries UAI_EDITOR_THEME (ADR-091); this value is the fallback for",
|
|
22
|
+
"//6": "tasks created before that shipped, or created outside a browser.",
|
|
19
23
|
"workbench.colorTheme": "Dark 2026",
|
|
20
24
|
|
|
21
|
-
"//
|
|
22
|
-
"//
|
|
23
|
-
"//
|
|
24
|
-
"//
|
|
25
|
+
"//7": "An editor, not an IDE (ADR-092). This pane is one of three in a",
|
|
26
|
+
"//8": "task's right panel and is often half a screen wide, so every bar it",
|
|
27
|
+
"//9": "does not need is width the code does. Cut here is chrome only: the",
|
|
28
|
+
"//10": "status bar, the layout buttons, the menu bar, the empty-editor",
|
|
29
|
+
"//11": "watermark, Open Editors, the minimap and the terminal's chat hint.",
|
|
30
|
+
"//12": "Breadcrumbs STAY: with the status bar gone they are the only thing",
|
|
31
|
+
"//13": "naming the path, and a tab reading 'route.ts' is worth little in a",
|
|
32
|
+
"//14": "repo with eleven of them.",
|
|
33
|
+
"workbench.statusBar.visible": false,
|
|
34
|
+
"workbench.layoutControl.enabled": false,
|
|
35
|
+
"window.menuBarVisibility": "hidden",
|
|
36
|
+
"workbench.editor.empty.hint": "hidden",
|
|
37
|
+
"explorer.openEditors.visible": 0,
|
|
38
|
+
"editor.minimap.enabled": false,
|
|
39
|
+
"terminal.integrated.initialHint": false,
|
|
40
|
+
|
|
41
|
+
"//15": "Hide the activity bar and its custom title strip. Explorer stays",
|
|
42
|
+
"//16": "open; the Editor pane's own toolbar provides the pointer route to",
|
|
43
|
+
"//17": "Terminal. Individual pins still live in the BROWSER's IndexedDB; the",
|
|
44
|
+
"//18": "guarded migration keeps the requested containers ready if a user",
|
|
45
|
+
"//19": "later restores the bar.",
|
|
46
|
+
"workbench.activityBar.location": "hidden",
|
|
47
|
+
|
|
48
|
+
"//22": "Workspace Trust — every task runs in an isolated container off the",
|
|
49
|
+
"//23": "user's own repos. The 'do you trust this folder?' prompt is pure",
|
|
50
|
+
"//24": "friction here; the user already trusts their code and the container",
|
|
51
|
+
"//25": "is the security boundary.",
|
|
25
52
|
"security.workspace.trust.enabled": false,
|
|
26
53
|
|
|
27
|
-
"//
|
|
28
|
-
"//
|
|
29
|
-
"//
|
|
30
|
-
"//
|
|
54
|
+
"//26": "Port forwarding — code-server's own auto-forward / 'Open in Browser'",
|
|
55
|
+
"//27": "popup can't reach anything useful here: dev servers are exposed",
|
|
56
|
+
"//28": "through uai's preview tunnel (ADR-025), not code-server's forwarder.",
|
|
57
|
+
"//29": "Disable it so the popup doesn't mislead.",
|
|
31
58
|
"remote.autoForwardPorts": false,
|
|
32
59
|
"remote.restoreForwardedPorts": false,
|
|
33
60
|
|
|
34
|
-
"//
|
|
61
|
+
"//30": "Integrated terminal uses zsh + oh-my-zsh (the node login shell).",
|
|
35
62
|
"terminal.integrated.defaultProfile.linux": "zsh"
|
|
36
63
|
}
|
|
@@ -219,11 +219,41 @@ cs_source_seed="$(dirname "${BASH_SOURCE[0]}")/code-server-settings.json"
|
|
|
219
219
|
if [ ! -f "$cs_seed" ] && [ -f "$cs_source_seed" ]; then
|
|
220
220
|
cs_seed="$cs_source_seed"
|
|
221
221
|
fi
|
|
222
|
+
|
|
223
|
+
# ADR-091: seed the Editor pane in the palette the task was created from.
|
|
224
|
+
# UAI_EDITOR_THEME carries a VS Code theme LABEL ("Quiet Light"), already
|
|
225
|
+
# resolved cloud-side from the theme registry — the container never maps a Uai
|
|
226
|
+
# palette id and never sees a raw user string. Unset is the normal state for a
|
|
227
|
+
# task created before this shipped or outside a browser; the seed's own baked
|
|
228
|
+
# default then stands.
|
|
229
|
+
#
|
|
230
|
+
# Applied ONLY while seeding. An existing settings.json is still left entirely
|
|
231
|
+
# alone, so a user's own editor tweaks survive a restart — which is also why
|
|
232
|
+
# changing the app's palette later does not re-theme an already-created task
|
|
233
|
+
# (docs/theming.md). If jq is missing or the filter fails, fall through to the
|
|
234
|
+
# plain copy: a themed editor is worth less than an editor that starts.
|
|
235
|
+
seed_code_server_settings() {
|
|
236
|
+
local seed="$1" dest="$2" theme="${UAI_EDITOR_THEME:-}"
|
|
237
|
+
if [ -n "$theme" ] && command -v jq >/dev/null 2>&1; then
|
|
238
|
+
if jq --arg theme "$theme" '.["workbench.colorTheme"] = $theme' \
|
|
239
|
+
"$seed" >"$dest.tmp" 2>/dev/null && [ -s "$dest.tmp" ]; then
|
|
240
|
+
mv "$dest.tmp" "$dest"
|
|
241
|
+
log "seeded code-server settings (theme: $theme)"
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
rm -f "$dest.tmp"
|
|
245
|
+
log "warning: could not apply UAI_EDITOR_THEME=$theme; using image default"
|
|
246
|
+
fi
|
|
247
|
+
cp "$seed" "$dest" || return 1
|
|
248
|
+
log "seeded code-server settings"
|
|
249
|
+
}
|
|
250
|
+
|
|
222
251
|
if [ ! -f "$cs_user_dir/settings.json" ]; then
|
|
223
252
|
if [ ! -f "$cs_seed" ]; then
|
|
224
253
|
log "warning: code-server settings seed missing at $cs_seed"
|
|
225
|
-
elif mkdir -p "$cs_user_dir" &&
|
|
226
|
-
|
|
254
|
+
elif mkdir -p "$cs_user_dir" &&
|
|
255
|
+
seed_code_server_settings "$cs_seed" "$cs_user_dir/settings.json"; then
|
|
256
|
+
:
|
|
227
257
|
else
|
|
228
258
|
log "warning: could not seed code-server settings at $cs_user_dir/settings.json"
|
|
229
259
|
fi
|
|
@@ -238,6 +268,7 @@ else
|
|
|
238
268
|
--disable-workspace-trust \
|
|
239
269
|
--auth none \
|
|
240
270
|
--disable-telemetry \
|
|
271
|
+
--disable-update-check \
|
|
241
272
|
--bind-addr 0.0.0.0:8080 \
|
|
242
273
|
"$editor_root" \
|
|
243
274
|
>/tmp/code-server.log 2>&1 &
|
package/lib/agent-cli.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* At task-up the host writes two files into the task workspace (bind-mounted at
|
|
5
5
|
* /workspace, like skills — no docker cp/exec needed):
|
|
6
6
|
* - `.uai/cli.mjs` — the self-contained agent CLI (Node ESM, fetch-only).
|
|
7
|
-
* - `.uai/uai.json` — { apiUrl
|
|
7
|
+
* - `.uai/uai.json` — shared, tokenless { apiUrl } config the CLI reads.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* as `node /workspace/.uai/cli.mjs <cmd>`
|
|
9
|
+
* A token is minted here per agent (host-side), carrying the task id, its owner
|
|
10
|
+
* user, that agent's identity, and only that agent's permissions; the cloud
|
|
11
|
+
* verifies it. Agents invoke the CLI as `node /workspace/.uai/cli.mjs <cmd>`
|
|
12
|
+
* (surfaced in the system preamble).
|
|
12
13
|
*/
|
|
13
14
|
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { resolve } from "node:path";
|
|
@@ -45,7 +46,7 @@ export function loadTaskCliSecret(taskId: string): string | null {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/** The union
|
|
49
|
+
/** The union used only to decide whether this task needs the shared CLI file. */
|
|
49
50
|
export function rosterPermissions(roster: RosterAgent[]): string[] {
|
|
50
51
|
return Array.from(new Set(roster.flatMap((a) => a.permissions ?? [])));
|
|
51
52
|
}
|
|
@@ -253,6 +254,29 @@ async function main() {
|
|
|
253
254
|
})).task);
|
|
254
255
|
break;
|
|
255
256
|
}
|
|
257
|
+
case "task close": {
|
|
258
|
+
const allowed = new Set(["status", "reason"]);
|
|
259
|
+
const unknown = Object.keys(flags).filter((flag) => !allowed.has(flag));
|
|
260
|
+
if (pos.length || unknown.length) {
|
|
261
|
+
console.error("uai: task close accepts only --status and --reason");
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
const status = flags.status;
|
|
265
|
+
if (status !== "finished" && status !== "canceled") {
|
|
266
|
+
console.error("uai: task close needs --status finished|canceled");
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
if (flags.reason === "true") {
|
|
270
|
+
console.error("uai: --reason needs text (quote multiword reasons)");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const closed = await api("POST", "/api/agent/tasks/close", {
|
|
274
|
+
status,
|
|
275
|
+
...(flags.reason ? { reason: String(flags.reason) } : {}),
|
|
276
|
+
});
|
|
277
|
+
out("task closed as " + closed.status);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
256
280
|
case "todo list": {
|
|
257
281
|
const todos = (await api("GET", "/api/agent/todos")).todos || [];
|
|
258
282
|
if (!todos.length) { out("punchlist is empty"); break; }
|
|
@@ -329,6 +353,7 @@ async function main() {
|
|
|
329
353
|
" uai people list",
|
|
330
354
|
" uai task list",
|
|
331
355
|
" uai task create --name <n> --prompt <p> [--projects id,id] [--team id] [--agents handle,handle]",
|
|
356
|
+
' uai task close --status finished|canceled [--reason "final report"]',
|
|
332
357
|
" uai memory search <query>",
|
|
333
358
|
" uai memory save <text> [--project id] [--tags a,b]",
|
|
334
359
|
" uai memory delete <id>",
|
|
@@ -249,14 +249,28 @@ export class DurableProcess {
|
|
|
249
249
|
this.exitHandlers.add(handler);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Serialise `value` as one JSONL line appended to the runner's inbox.
|
|
254
|
+
* A runner-filtered metadata line immediately before it supplies the local
|
|
255
|
+
* timestamp without changing the protocol frame forwarded to the CLI. Old
|
|
256
|
+
* runners already ignore unknown `__uai` lines, so attached sessions remain
|
|
257
|
+
* wire-compatible across the host update.
|
|
258
|
+
*
|
|
259
|
+
* `__uai` MUST stay the first key of the metadata object. Both runners filter
|
|
260
|
+
* with `line.startsWith('{"__uai"')` — a byte prefix, not a parse — so
|
|
261
|
+
* serialising the keys in any other order emits `{"ts":…`, which the runner
|
|
262
|
+
* then forwards to the CLI's stdin as a user turn. No type in this file
|
|
263
|
+
* expresses that, and a `toEqual` assertion cannot see it; the raw-prefix
|
|
264
|
+
* check in `durable-proc.test.ts` is what pins it.
|
|
265
|
+
*/
|
|
253
266
|
writeLine(value: unknown): void {
|
|
254
267
|
if (this.closed || this.detached) return;
|
|
255
268
|
const json = JSON.stringify(value);
|
|
269
|
+
const inputMeta = JSON.stringify({ __uai: "input", ts: Date.now() });
|
|
256
270
|
if (this.debug) this.log(`-> ${json.slice(0, 1000)}`);
|
|
257
271
|
// Chain appends so concurrent writes can't interleave bytes.
|
|
258
272
|
this.inboxChain = this.inboxChain
|
|
259
|
-
.then(() => fsp.appendFile(this.inboxPath, `${json}\n`))
|
|
273
|
+
.then(() => fsp.appendFile(this.inboxPath, `${inputMeta}\n${json}\n`))
|
|
260
274
|
.catch(() => {
|
|
261
275
|
// Disk error — liveness checks will surface a dead session.
|
|
262
276
|
});
|
package/lib/agents/transport.ts
CHANGED
|
@@ -22,8 +22,16 @@
|
|
|
22
22
|
* feature, and each spawn ships the runner version matching this host.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
25
|
+
import {
|
|
26
|
+
copyFileSync,
|
|
27
|
+
mkdirSync,
|
|
28
|
+
renameSync,
|
|
29
|
+
rmSync,
|
|
30
|
+
statSync,
|
|
31
|
+
symlinkSync,
|
|
32
|
+
promises as fsp,
|
|
33
|
+
} from "node:fs";
|
|
34
|
+
import { basename, dirname, join } from "node:path";
|
|
27
35
|
|
|
28
36
|
import { and, eq } from "drizzle-orm";
|
|
29
37
|
|
|
@@ -93,6 +101,7 @@ function durableEnabled(): boolean {
|
|
|
93
101
|
|
|
94
102
|
export function createAgentTransport(opts: AgentTransportOptions): LineTransport {
|
|
95
103
|
if (!durableEnabled()) {
|
|
104
|
+
clearCurrentSession(opts.taskId, opts.agentId);
|
|
96
105
|
const { command, args } = dockerExecArgs(
|
|
97
106
|
opts.containerName,
|
|
98
107
|
opts.cli,
|
|
@@ -157,6 +166,7 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
157
166
|
debugLabel: opts.debugLabel,
|
|
158
167
|
});
|
|
159
168
|
proc.onExit(markClosed);
|
|
169
|
+
publishCurrentSession(row.sessionDir);
|
|
160
170
|
return claimTail(tailKey, proc);
|
|
161
171
|
}
|
|
162
172
|
|
|
@@ -194,6 +204,7 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
194
204
|
console.warn(
|
|
195
205
|
`[transport] runner unavailable (${err instanceof Error ? err.message : err}) — falling back to direct pipes for ${opts.agentId}`,
|
|
196
206
|
);
|
|
207
|
+
clearCurrentSession(opts.taskId, opts.agentId);
|
|
197
208
|
const { command, args } = dockerExecArgs(
|
|
198
209
|
opts.containerName,
|
|
199
210
|
opts.cli,
|
|
@@ -263,9 +274,49 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
263
274
|
.run();
|
|
264
275
|
|
|
265
276
|
proc.onExit(markClosed);
|
|
277
|
+
publishCurrentSession(sessionDir);
|
|
266
278
|
return claimTail(tailKey, proc);
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Make the opaque generation directory discoverable from inside the task.
|
|
283
|
+
* The relative link resolves on both sides of the workspace bind mount. The
|
|
284
|
+
* pointer is diagnostic only: failure to publish it must not kill a session.
|
|
285
|
+
*/
|
|
286
|
+
function publishCurrentSession(sessionDir: string): void {
|
|
287
|
+
const parent = dirname(sessionDir);
|
|
288
|
+
const current = join(parent, "current");
|
|
289
|
+
const pending = join(parent, `.current-${process.pid}-${Date.now()}`);
|
|
290
|
+
try {
|
|
291
|
+
symlinkSync(basename(sessionDir), pending, "dir");
|
|
292
|
+
renameSync(pending, current);
|
|
293
|
+
} catch {
|
|
294
|
+
try {
|
|
295
|
+
rmSync(pending, { force: true });
|
|
296
|
+
} catch {
|
|
297
|
+
// Best-effort navigation aid; the generation path remains authoritative.
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** A direct-pipe session must not leave an older durable inbox looking live. */
|
|
303
|
+
function clearCurrentSession(taskId: string, agentId: string): void {
|
|
304
|
+
try {
|
|
305
|
+
rmSync(
|
|
306
|
+
join(
|
|
307
|
+
taskWorkspaceDir(taskId),
|
|
308
|
+
".uai",
|
|
309
|
+
"sessions",
|
|
310
|
+
agentId,
|
|
311
|
+
"current",
|
|
312
|
+
),
|
|
313
|
+
{ force: true },
|
|
314
|
+
);
|
|
315
|
+
} catch {
|
|
316
|
+
// Same best-effort rule as publishCurrentSession.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
269
320
|
function heartbeatFresh(sessionDir: string): boolean {
|
|
270
321
|
try {
|
|
271
322
|
return (
|
package/lib/orchestrator.ts
CHANGED
|
@@ -2805,9 +2805,10 @@ export function assembleFirstTurnPrompt(
|
|
|
2805
2805
|
|
|
2806
2806
|
/**
|
|
2807
2807
|
* Build the system preamble an agent gets on session start. It opens
|
|
2808
|
-
* with how uai's channel works
|
|
2809
|
-
*
|
|
2810
|
-
*
|
|
2808
|
+
* with how uai's channel works. Open-mode agents hand off via `@mention`
|
|
2809
|
+
* because there is no `peer` command / shared tmux (ADR-008); Secretary-mode
|
|
2810
|
+
* crew report through the designated Secretary instead. It then appends the
|
|
2811
|
+
* project context and the agent's persona.
|
|
2811
2812
|
*
|
|
2812
2813
|
* The persona / mission layers (project defaultPrompts, globalContext,
|
|
2813
2814
|
* agent.defaultPrompt) live in the always-on system prompt so they apply
|
|
@@ -2832,6 +2833,12 @@ export function buildSystemPreamble(
|
|
|
2832
2833
|
a.id === agent.id ? `@${a.id} (${a.label}, you)` : `@${a.id} (${a.label})`,
|
|
2833
2834
|
)
|
|
2834
2835
|
.join(", ");
|
|
2836
|
+
const isSecretary =
|
|
2837
|
+
mode === "secretary" && secretaryAgentId === agent.id;
|
|
2838
|
+
const isSecretaryCrew =
|
|
2839
|
+
mode === "secretary" &&
|
|
2840
|
+
secretaryAgentId !== undefined &&
|
|
2841
|
+
!isSecretary;
|
|
2835
2842
|
// ADR-049: with several humans in the chat, brief the agent on who they are
|
|
2836
2843
|
// and how to address one specifically. Single-human tasks keep the original
|
|
2837
2844
|
// wording byte-identical.
|
|
@@ -2839,8 +2846,14 @@ export function buildSystemPreamble(
|
|
|
2839
2846
|
const humanList = (humans ?? [])
|
|
2840
2847
|
.map((h) => `@${h.handle} (${h.name}${h.isOwner ? ", task owner" : ""})`)
|
|
2841
2848
|
.join(", ");
|
|
2842
|
-
const humanIntro =
|
|
2849
|
+
const humanIntro = isSecretaryCrew
|
|
2843
2850
|
? [
|
|
2851
|
+
`The human-facing conversation is owned by @${secretaryAgentId}, the`,
|
|
2852
|
+
"designated Secretary. `@you` and human @handles in your replies remain",
|
|
2853
|
+
"visible as routing hints, but they do not notify a human directly.",
|
|
2854
|
+
]
|
|
2855
|
+
: multiHuman
|
|
2856
|
+
? [
|
|
2844
2857
|
`SEVERAL humans share this channel: ${humanList}. Their messages`,
|
|
2845
2858
|
"arrive prefixed with the sender's name so you can tell them apart.",
|
|
2846
2859
|
"`@you` still works and reaches the human you're currently talking",
|
|
@@ -2855,8 +2868,8 @@ export function buildSystemPreamble(
|
|
|
2855
2868
|
"just asked, acknowledgments — post in the channel WITHOUT",
|
|
2856
2869
|
"@-mentioning; they can read the channel and don't need a ping for",
|
|
2857
2870
|
"every message.",
|
|
2858
|
-
|
|
2859
|
-
|
|
2871
|
+
]
|
|
2872
|
+
: [
|
|
2860
2873
|
"The human you're working with is **@you**. @-mentioning them sends a",
|
|
2861
2874
|
"NOTIFICATION, so use it sparingly — only when you actually need them: a",
|
|
2862
2875
|
"decision you can't make, a blocker, an approval, or you've finished your",
|
|
@@ -2866,7 +2879,7 @@ export function buildSystemPreamble(
|
|
|
2866
2879
|
"something they just asked, acknowledgments — post in the channel WITHOUT",
|
|
2867
2880
|
"@-mentioning @you; they can read the channel and don't need a ping for",
|
|
2868
2881
|
"every message. Do NOT reflexively end messages with @you.",
|
|
2869
|
-
|
|
2882
|
+
];
|
|
2870
2883
|
const projectLines =
|
|
2871
2884
|
projects.length === 0
|
|
2872
2885
|
? ["(none mounted)"]
|
|
@@ -2874,8 +2887,6 @@ export function buildSystemPreamble(
|
|
|
2874
2887
|
(p) =>
|
|
2875
2888
|
`- \`${workspacePath}/${p.slug}\` — git worktree on \`${taskBranch}\``,
|
|
2876
2889
|
);
|
|
2877
|
-
const isSecretary =
|
|
2878
|
-
mode === "secretary" && secretaryAgentId === agent.id;
|
|
2879
2890
|
const dispatchActionBrief = [
|
|
2880
2891
|
"When crew work is needed, run the Secretary CLI action:",
|
|
2881
2892
|
`\`node ${CONTAINER_CLI_PATH} dispatch @agent [@agent…] \"instruction\"\``,
|
|
@@ -2911,6 +2922,22 @@ export function buildSystemPreamble(
|
|
|
2911
2922
|
"`/workspace/.uai/chat.md`. Read it whenever you need that context;",
|
|
2912
2923
|
"it's appended live, so re-read it for the latest.",
|
|
2913
2924
|
];
|
|
2925
|
+
const sessionInboxBrief =
|
|
2926
|
+
mode !== "secretary"
|
|
2927
|
+
? []
|
|
2928
|
+
: isSecretary
|
|
2929
|
+
? [
|
|
2930
|
+
"When durable sessions are active, their current inboxes are linked at",
|
|
2931
|
+
"`/workspace/.uai/sessions/<agent-id>/current/inbox.jsonl`.",
|
|
2932
|
+
'An epoch-ms `{"__uai":"input","ts":…}` line precedes each',
|
|
2933
|
+
"unchanged CLI frame, so the adjacent pair identifies a wake input.",
|
|
2934
|
+
]
|
|
2935
|
+
: [
|
|
2936
|
+
"When durable sessions are active, your current runner inbox is at",
|
|
2937
|
+
`\`/workspace/.uai/sessions/${agent.id}/current/inbox.jsonl\`.`,
|
|
2938
|
+
'An epoch-ms `{"__uai":"input","ts":…}` line precedes each',
|
|
2939
|
+
"unchanged CLI frame, so the adjacent pair identifies a wake input.",
|
|
2940
|
+
];
|
|
2914
2941
|
const secretaryRoleBrief = isSecretary
|
|
2915
2942
|
? [
|
|
2916
2943
|
"## Secretary role",
|
|
@@ -2939,7 +2966,14 @@ export function buildSystemPreamble(
|
|
|
2939
2966
|
"instruction for each",
|
|
2940
2967
|
"crew member you need. Do not merely say that someone else will answer.",
|
|
2941
2968
|
]
|
|
2942
|
-
:
|
|
2969
|
+
: isSecretaryCrew
|
|
2970
|
+
? [
|
|
2971
|
+
"Handles in your dispatch or the backstage transcript are context,",
|
|
2972
|
+
"not proof that another recipient was notified. Answer only for your",
|
|
2973
|
+
"assigned part. If someone else should act, state who and what is",
|
|
2974
|
+
"needed; the Secretary decides whether to dispatch it.",
|
|
2975
|
+
]
|
|
2976
|
+
: [
|
|
2943
2977
|
"When a message already @-mentions several participants at once (the",
|
|
2944
2978
|
"human asking the whole group, or a peer addressing multiple agents),",
|
|
2945
2979
|
"it's a group broadcast — this is a GROUP CHAT and everyone named has",
|
|
@@ -2949,14 +2983,20 @@ export function buildSystemPreamble(
|
|
|
2949
2983
|
"turn`, or `still waiting on @x`. Re-mentioning someone who already got",
|
|
2950
2984
|
"the message only wakes them again and spirals into duplicate replies.",
|
|
2951
2985
|
"Say your piece and stop.",
|
|
2952
|
-
|
|
2986
|
+
];
|
|
2953
2987
|
const handoffBrief = isSecretary
|
|
2954
2988
|
? [
|
|
2955
2989
|
"When crew work finishes, synthesize the outcome for the human and make",
|
|
2956
2990
|
"the next decision or blocker explicit. Do not abandon an unresolved",
|
|
2957
2991
|
"request silently, and do not wake a peer for acknowledgments alone.",
|
|
2958
2992
|
]
|
|
2959
|
-
:
|
|
2993
|
+
: isSecretaryCrew
|
|
2994
|
+
? [
|
|
2995
|
+
"When you finish, report the result, next decision, or blocker plainly",
|
|
2996
|
+
"to the Secretary; no @mention is needed. Do not assume a named peer",
|
|
2997
|
+
"was woken, and do not wait for a direct reply from one.",
|
|
2998
|
+
]
|
|
2999
|
+
: [
|
|
2960
3000
|
"Hand off when you finish your part of the work. When you've made",
|
|
2961
3001
|
"and committed your changes, or completed a review, end your reply by",
|
|
2962
3002
|
"@-mentioning the agent who should act next and telling them what you",
|
|
@@ -2966,7 +3006,7 @@ export function buildSystemPreamble(
|
|
|
2966
3006
|
"peer needs to act, it's fine to stop; only @-mention @you if you need",
|
|
2967
3007
|
"their input or are handing back finished work for them to act on. Don't",
|
|
2968
3008
|
"prolong an agent-to-agent exchange just to fill silence.",
|
|
2969
|
-
|
|
3009
|
+
];
|
|
2970
3010
|
const checkInTranscriptBrief = isSecretary
|
|
2971
3011
|
? [
|
|
2972
3012
|
"Read both transcript files named above, and speak ONLY if you have",
|
|
@@ -2974,11 +3014,13 @@ export function buildSystemPreamble(
|
|
|
2974
3014
|
"PASS reply is discarded and never shown to anyone, so it is always a",
|
|
2975
3015
|
"safe way to decline a turn.",
|
|
2976
3016
|
]
|
|
2977
|
-
:
|
|
3017
|
+
: isSecretaryCrew
|
|
3018
|
+
? []
|
|
3019
|
+
: [
|
|
2978
3020
|
"Read the transcript, and speak ONLY if you have something substantive to",
|
|
2979
3021
|
"add; otherwise reply with exactly `PASS` — a PASS reply is discarded and",
|
|
2980
3022
|
"never shown to anyone, so it is always a safe way to decline a turn.",
|
|
2981
|
-
|
|
3023
|
+
];
|
|
2982
3024
|
const workspaceBrief = isSecretary
|
|
2983
3025
|
? [
|
|
2984
3026
|
"## Workspace layout",
|
|
@@ -3028,19 +3070,35 @@ export function buildSystemPreamble(
|
|
|
3028
3070
|
"or @-mentioning a crew agent in prose does NOT wake them.",
|
|
3029
3071
|
"The only way to hand crew work off is the structured `dispatch` action.",
|
|
3030
3072
|
]
|
|
3031
|
-
:
|
|
3073
|
+
: isSecretaryCrew
|
|
3074
|
+
? [
|
|
3075
|
+
"You are a backstage crew agent in a Secretary-mode task channel.",
|
|
3076
|
+
`@${secretaryAgentId} is the designated Secretary and your sole`,
|
|
3077
|
+
"conversational routing point. Every completed reply you write is",
|
|
3078
|
+
`delivered once to @${secretaryAgentId}, whether it names @you, a`,
|
|
3079
|
+
"human, another crew agent, or nobody. Those names remain visible as",
|
|
3080
|
+
"routing hints, but they do not notify the human or wake a peer.",
|
|
3081
|
+
]
|
|
3082
|
+
: [
|
|
3032
3083
|
"You are one agent in a uai task chat channel, shared with the human",
|
|
3033
3084
|
"and the other agents. To hand work to or ask another agent, mention",
|
|
3034
3085
|
"it by id at the start of a line — e.g. `@codex please review the",
|
|
3035
3086
|
"diff`. uai routes that message into that agent's input.",
|
|
3036
|
-
|
|
3087
|
+
];
|
|
3037
3088
|
const collaborationBrief = isSecretary
|
|
3038
3089
|
? [
|
|
3039
3090
|
"Collaborate through deliberate dispatches. Each dispatch wakes a crew",
|
|
3040
3091
|
"agent and costs a turn, so make every instruction concrete, self-contained,",
|
|
3041
3092
|
"and necessary. Never dispatch greetings, thanks, or acknowledgments.",
|
|
3042
3093
|
]
|
|
3043
|
-
:
|
|
3094
|
+
: isSecretaryCrew
|
|
3095
|
+
? [
|
|
3096
|
+
`Only a structured dispatch from @${secretaryAgentId} wakes a crew`,
|
|
3097
|
+
"agent. Work on the assignment you received. If someone else should",
|
|
3098
|
+
"act, state who and what is needed; the Secretary decides whether to",
|
|
3099
|
+
"dispatch it.",
|
|
3100
|
+
]
|
|
3101
|
+
: [
|
|
3044
3102
|
"An agent only receives a message when it is explicitly @-mentioned",
|
|
3045
3103
|
"(or addressed by the human) — so always @-mention the agent (or @you)",
|
|
3046
3104
|
"you mean. There is NO `peer` command and no shared tmux session;",
|
|
@@ -3059,7 +3117,7 @@ export function buildSystemPreamble(
|
|
|
3059
3117
|
"don't @-mention back. And when there's no active task yet (intros, or",
|
|
3060
3118
|
"you're waiting on the human), answer briefly and then wait — you don't",
|
|
3061
3119
|
"need to @-mention anyone (including @you); they can see the channel.",
|
|
3062
|
-
|
|
3120
|
+
];
|
|
3063
3121
|
const channelConventionsBrief = isSecretary
|
|
3064
3122
|
? [
|
|
3065
3123
|
"Your prose always goes to the human-facing lane; answer plainly without",
|
|
@@ -3068,13 +3126,19 @@ export function buildSystemPreamble(
|
|
|
3068
3126
|
"You may occasionally receive a `[channel check-in]` asking you to catch",
|
|
3069
3127
|
"up on the channel.",
|
|
3070
3128
|
]
|
|
3071
|
-
:
|
|
3129
|
+
: isSecretaryCrew
|
|
3130
|
+
? [
|
|
3131
|
+
"Every reply returns to the Secretary whether or not it contains",
|
|
3132
|
+
"@mentions. Answer plainly; use names only when they help the",
|
|
3133
|
+
"Secretary understand who should hear or act on the result.",
|
|
3134
|
+
]
|
|
3135
|
+
: [
|
|
3072
3136
|
"Two channel conventions (ADR-050): (1) If your reply @-mentions nobody,",
|
|
3073
3137
|
"uai hands it back to whoever prompted you — so when you're ANSWERING,",
|
|
3074
3138
|
"just answer plainly; you don't need to re-mention the asker. Mention",
|
|
3075
3139
|
"someone only to bring them in or hand work off. (2) You may occasionally",
|
|
3076
3140
|
"receive a `[channel check-in]` asking you to catch up on the channel.",
|
|
3077
|
-
|
|
3141
|
+
];
|
|
3078
3142
|
const comms = [
|
|
3079
3143
|
"## uai task channel",
|
|
3080
3144
|
"",
|
|
@@ -3094,6 +3158,8 @@ export function buildSystemPreamble(
|
|
|
3094
3158
|
"",
|
|
3095
3159
|
...transcriptBrief,
|
|
3096
3160
|
"",
|
|
3161
|
+
...sessionInboxBrief,
|
|
3162
|
+
...(sessionInboxBrief.length > 0 ? [""] : []),
|
|
3097
3163
|
...secretaryRoleBrief,
|
|
3098
3164
|
...channelConventionsBrief,
|
|
3099
3165
|
...checkInTranscriptBrief,
|
|
@@ -3191,6 +3257,9 @@ export function buildSystemPreamble(
|
|
|
3191
3257
|
(agent.permissions?.includes("tasks.create")
|
|
3192
3258
|
? ", `task create --name <n> --prompt <p> [--projects id,id] [--team id] [--agents handle,handle]`"
|
|
3193
3259
|
: "") +
|
|
3260
|
+
(agent.permissions?.includes("tasks.close")
|
|
3261
|
+
? ", `task close --status finished|canceled [--reason \"final report\"]`"
|
|
3262
|
+
: "") +
|
|
3194
3263
|
(agent.permissions?.includes("memory.write")
|
|
3195
3264
|
? ", `memory save <text>`"
|
|
3196
3265
|
: "") +
|
|
@@ -3227,6 +3296,17 @@ export function buildSystemPreamble(
|
|
|
3227
3296
|
"`projects list` / `people list`.",
|
|
3228
3297
|
]
|
|
3229
3298
|
: []),
|
|
3299
|
+
...(agent.permissions?.includes("tasks.close")
|
|
3300
|
+
? [
|
|
3301
|
+
"**Closing this task:** `task close` is task-wide, not a way to",
|
|
3302
|
+
"say that only your part is done. It stops every agent and",
|
|
3303
|
+
"destroys this container/session. Use it only when the ENTIRE",
|
|
3304
|
+
"task is terminal and the human's instructions authorize that",
|
|
3305
|
+
"outcome. Make it the final action of your turn and put the",
|
|
3306
|
+
"useful final summary in `--reason`, because your normal response",
|
|
3307
|
+
"may not be delivered after the container tears down.",
|
|
3308
|
+
]
|
|
3309
|
+
: []),
|
|
3230
3310
|
...(agent.permissions?.includes("memory.read") ||
|
|
3231
3311
|
agent.permissions?.includes("memory.write")
|
|
3232
3312
|
? [
|
package/package.json
CHANGED
package/runner/runner.mjs
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* <sessionDir>/runner.json pid, protocol, argv, startedAt
|
|
12
12
|
*
|
|
13
13
|
* Meta lines are `{"__uai":"spawn"|"exit", ...}`; the host filters them out
|
|
14
|
-
* before handing lines to the protocol adapters.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* before handing lines to the protocol adapters. Host metadata/control lines
|
|
15
|
+
* in the inbox use the same shape (`input` timestamps and `stop`); the runner
|
|
16
|
+
* filters those, while every protocol frame goes to the CLI's stdin untouched.
|
|
17
17
|
*
|
|
18
18
|
* Plain Node ≥18, dependency-free, ESM. Testable outside docker: point it
|
|
19
19
|
* at a tmp dir and any line-oriented fake CLI.
|
package/scripts/agent/task-up.sh
CHANGED
|
@@ -1081,6 +1081,13 @@ fi
|
|
|
1081
1081
|
# Projects may declare managed names, so skip them here rather than trust
|
|
1082
1082
|
# callers. They are also removed from the in-memory Compose override below.
|
|
1083
1083
|
#
|
|
1084
|
+
# UAI_EDITOR_THEME is reserved in BOTH places for the case that is easy to
|
|
1085
|
+
# miss: a task whose stored palette is null, on a project that happens to
|
|
1086
|
+
# declare that name. The cloud sends no theme, so the `preview_env` entry
|
|
1087
|
+
# that normally shadows a declared key is absent too — and without these two
|
|
1088
|
+
# lines a project would get to pick what the Editor pane looks like. Reserved,
|
|
1089
|
+
# the name is not emitted at all and uai-init seeds the image's own default.
|
|
1090
|
+
#
|
|
1084
1091
|
# Project values never enter the host environment. Emit an explicit empty
|
|
1085
1092
|
# default for every declared key; the stdin-only Compose override supplies
|
|
1086
1093
|
# decrypted values at `compose up` time.
|
|
@@ -1094,15 +1101,23 @@ fi
|
|
|
1094
1101
|
GIT_SSH | GIT_SSH_COMMAND | HOME | XDG_CONFIG_HOME | XDG_DATA_HOME | \
|
|
1095
1102
|
BASH_ENV | ENV | UAI_GIT_TRANSPORT | \
|
|
1096
1103
|
CLAUDE_CODE_OAUTH_TOKEN | ANTHROPIC_API_KEY | \
|
|
1097
|
-
ANTHROPIC_AUTH_TOKEN | XAI_API_KEY | PLAYWRIGHT_BROWSERS_PATH
|
|
1104
|
+
ANTHROPIC_AUTH_TOKEN | XAI_API_KEY | PLAYWRIGHT_BROWSERS_PATH | \
|
|
1105
|
+
UAI_EDITOR_THEME) continue ;;
|
|
1098
1106
|
esac
|
|
1099
1107
|
printf ' %s: ""\n' "$ekey"
|
|
1100
1108
|
done < <(jq -r '.[]' <<<"$union_env_keys_json")
|
|
1101
|
-
#
|
|
1102
|
-
#
|
|
1103
|
-
#
|
|
1104
|
-
#
|
|
1105
|
-
#
|
|
1109
|
+
# Cloud-computed literal env (ADR-025): a JSON object {VAR: value} on the
|
|
1110
|
+
# task row. Non-secret, so written LITERALLY (unlike the ${KEY:-}
|
|
1111
|
+
# pass-throughs above). Emitted last so it wins over any same-named declared
|
|
1112
|
+
# key. Two producers, both cloud-side and both opaque here:
|
|
1113
|
+
# - PUBLIC preview URLs under operator-chosen names (EXPO_PACKAGER_PROXY_URL
|
|
1114
|
+
# and friends), one per preview port that set `urlEnv`;
|
|
1115
|
+
# - UAI_EDITOR_THEME (ADR-091), the Editor pane's colour theme as a VS Code
|
|
1116
|
+
# theme LABEL ("Quiet Light"), already resolved from the palette the
|
|
1117
|
+
# creating browser snapshotted. The host never maps a Uai palette id.
|
|
1118
|
+
# UAI_EDITOR_THEME is absent for every task created before it shipped and for
|
|
1119
|
+
# any created outside a browser; uai-init then seeds the image's own default,
|
|
1120
|
+
# so the column staying null is a supported state, not a value to fill in.
|
|
1106
1121
|
preview_env_raw=$(jq -r '.[0].preview_env // "{}"' <<<"$task_json")
|
|
1107
1122
|
while IFS= read -r pe_entry; do
|
|
1108
1123
|
[ -n "$pe_entry" ] || continue
|
|
@@ -1188,7 +1203,8 @@ compose_env_override=$(printf '%s' "$project_env_json" | jq -c \
|
|
|
1188
1203
|
.ANTHROPIC_API_KEY,
|
|
1189
1204
|
.ANTHROPIC_AUTH_TOKEN,
|
|
1190
1205
|
.XAI_API_KEY,
|
|
1191
|
-
.PLAYWRIGHT_BROWSERS_PATH
|
|
1206
|
+
.PLAYWRIGHT_BROWSERS_PATH,
|
|
1207
|
+
.UAI_EDITOR_THEME
|
|
1192
1208
|
)
|
|
1193
1209
|
| delpaths($preview | keys | map([.]))
|
|
1194
1210
|
| {services:{app:{environment:.}}}
|
package/src/protocol.ts
CHANGED
|
@@ -43,6 +43,14 @@ export const MAX_SECRETARY_DISPATCH_ID_CHARS = 256;
|
|
|
43
43
|
|
|
44
44
|
export interface CommandContext {
|
|
45
45
|
commandId: string;
|
|
46
|
+
/**
|
|
47
|
+
* Cloud-side deadline for this command, in ms. Optional and OFF by default:
|
|
48
|
+
* most commands (task-up especially) legitimately take minutes, so a blanket
|
|
49
|
+
* timeout would be wrong. When set, the cloud stops waiting AND forgets the
|
|
50
|
+
* pending entry — without that second half a silent host accumulates one
|
|
51
|
+
* dead resolver per retry.
|
|
52
|
+
*/
|
|
53
|
+
timeoutMs?: number;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export type PermissionDecision = { kind: "accept" } | { kind: "decline" };
|
|
@@ -191,11 +199,17 @@ export interface TaskCommandTask {
|
|
|
191
199
|
reviewerOrder?: string[];
|
|
192
200
|
agents: TaskAgent[];
|
|
193
201
|
/**
|
|
194
|
-
* Cloud-computed
|
|
195
|
-
*
|
|
196
|
-
* `
|
|
197
|
-
*
|
|
198
|
-
*
|
|
202
|
+
* Cloud-computed literal env to inject into the container at task-up.
|
|
203
|
+
* Non-secret by construction — written straight into the task's compose file,
|
|
204
|
+
* unlike the `${KEY:-}` pass-throughs used for credentials. Only present on
|
|
205
|
+
* task-up commands.
|
|
206
|
+
*
|
|
207
|
+
* Named for its first user, PUBLIC preview URLs keyed by the operator-chosen
|
|
208
|
+
* var name (e.g. `EXPO_PACKAGER_PROXY_URL`, present when a preview port set
|
|
209
|
+
* `urlEnv` and the cloud has a preview base domain). It now also carries
|
|
210
|
+
* `UAI_EDITOR_THEME` (ADR-091) — same shape, same guarantees, same one-way
|
|
211
|
+
* trip to `services.app.environment`, so it rides here rather than growing
|
|
212
|
+
* the protocol a second env channel. The host treats every entry as opaque.
|
|
199
213
|
*/
|
|
200
214
|
previewEnv?: Record<string, string>;
|
|
201
215
|
/** ADR-062: owning org — locates the org shared-files root on the host. */
|