@kingcrab/pi-imessage 1.1.1 → 1.2.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/README.md +4 -6
- package/dist/agent/behavior.js +7 -13
- package/dist/agent/chats.js +7 -8
- package/dist/agent/commands.js +16 -11
- package/dist/agent/harness.js +6 -0
- package/dist/agent/progress.js +30 -4
- package/dist/agent/prompt.js +41 -24
- package/dist/agent/run.js +5 -4
- package/dist/agent/scheduling.js +11 -34
- package/dist/cli.js +42 -5
- package/dist/config/settings.js +4 -0
- package/dist/extensions/browser.js +7 -3
- package/dist/extensions/codemode.js +16 -0
- package/dist/extensions/final-text.js +12 -0
- package/dist/extensions/schedules.js +1 -1
- package/dist/extensions/subagent.js +3 -12
- package/dist/extensions/workspace.js +6 -2
- package/dist/main.js +42 -49
- package/dist/transport/watch.js +7 -1
- package/dist/web/app.js +63 -121
- package/dist/web/server.js +7 -14
- package/dist/web/style.css +0 -6
- package/dist/web/tasks.js +12 -120
- package/docs/adr/{0004-native-extensions.md → 0003-native-extensions.md} +1 -1
- package/docs/adr/{0006-jsonl-storage.md → 0004-jsonl-storage.md} +1 -1
- package/docs/adr/{0007-images-by-path.md → 0005-images-by-path.md} +1 -1
- package/docs/adr/{0010-test-budget.md → 0006-test-budget.md} +1 -1
- package/docs/adr/{0011-reconcile-unsent-answers.md → 0007-reconcile-unsent-answers.md} +2 -2
- package/docs/adr/{0012-run-in-owned-conversation.md → 0008-run-in-owned-conversation.md} +5 -5
- package/docs/adr/{0032-native-background-schedules.md → 0009-native-background-schedules.md} +1 -1
- package/docs/adr/0010-framework-and-workspace.md +27 -0
- package/docs/adr/README.md +24 -48
- package/docs/api.md +16 -8
- package/docs/architecture.md +19 -8
- package/docs/browser.md +7 -7
- package/docs/install.md +109 -0
- package/docs/workspace-extensions.md +8 -4
- package/package.json +4 -5
- package/dist/agent/compaction.js +0 -51
- package/dist/agent/error-log.js +0 -37
- package/dist/extensions/restart.js +0 -100
- package/docs/adr/0003-replies-at-most-once.md +0 -11
- package/docs/adr/0005-history-migration.md +0 -11
- package/docs/adr/0008-compaction-and-run.md +0 -11
- package/docs/adr/0009-one-shot-cutover.md +0 -11
- package/docs/adr/0013-thin-adapters-around-durable.md +0 -18
- package/docs/adr/0014-ephemeral-private-storage.md +0 -13
- package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +0 -21
- package/docs/adr/0016-process-http-transport.md +0 -21
- package/docs/adr/0017-browser-cli-owned-scope.md +0 -22
- package/docs/adr/0018-tasks-without-prompt-api.md +0 -19
- package/docs/adr/0019-remove-scheduled-services.md +0 -41
- package/docs/adr/0020-chat-task-progress.md +0 -41
- package/docs/adr/0021-readable-progress-without-link-previews.md +0 -33
- package/docs/adr/0022-progress-admission-and-settlement.md +0 -35
- package/docs/adr/0023-retain-progress-type-prefixes.md +0 -18
- package/docs/adr/0024-status-marker-before-type-prefix.md +0 -13
- package/docs/adr/0025-reusable-progress-and-command-preview.md +0 -49
- package/docs/adr/0026-inline-command-only-requests.md +0 -16
- package/docs/adr/0027-command-preview-width.md +0 -10
- package/docs/adr/0028-command-preview-120.md +0 -8
- package/docs/adr/0030-shared-web-read-cache.md +0 -33
- package/docs/adr/0031-quiet-compaction-progress.md +0 -32
- package/docs/adr/0033-operator-scheduled-run.md +0 -23
- package/docs/adr/0034-machine-local-schedule-time.md +0 -36
- package/docs/adr/0035-external-business-task-extensions.md +0 -56
- package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +0 -37
- package/docs/adr/0037-restart-during-in-flight-work.md +0 -30
- package/docs/adr/0038-restart-any-time.md +0 -25
- package/docs/adr/0039-rerun-interrupted-tools.md +0 -24
- package/docs/adr/0040-chat-progress-notices.md +0 -44
- package/docs/adr/0041-restart-and-recovery.md +0 -28
- package/docs/adr/0042-background-schedules-and-workspace-extensions.md +0 -44
- package/docs/adr/0043-remove-legacy-migration.md +0 -23
- package/docs/adr/0044-message-channels.md +0 -32
- package/docs/adr/0045-chat-scheduling-tools.md +0 -51
- package/docs/adr/0046-selective-command-redaction.md +0 -31
- package/docs/adr/0047-one-scheduling-path.md +0 -158
- package/docs/adr/0048-quiet-attachment-reads.md +0 -11
- package/docs/adr/0049-per-chat-progress-notices.md +0 -21
- package/docs/adr/0052-host-owned-self-restart.md +0 -53
- package/docs/adr/0053-service-log-errors.md +0 -44
- package/ops/README.md +0 -250
- package/ops/preflight.mjs +0 -28
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** Text written next to tool calls in one assistant message: the model's interim update. */
|
|
2
|
+
export function interimText(message) {
|
|
3
|
+
if (message?.role !== "assistant" || message.stopReason !== "toolUse")
|
|
4
|
+
return;
|
|
5
|
+
if (!message.content.some((part) => part.type === "toolCall"))
|
|
6
|
+
return;
|
|
7
|
+
const text = message.content
|
|
8
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
9
|
+
.join("\n")
|
|
10
|
+
.trim();
|
|
11
|
+
return text || undefined;
|
|
12
|
+
}
|
|
1
13
|
/** The same final-only projection is used for transport and the model's delivery facts. */
|
|
2
14
|
export function finalReplyText(message) {
|
|
3
15
|
if (message?.role !== "assistant" || !["stop", "length"].includes(message.stopReason))
|
|
@@ -8,7 +8,7 @@ export const ScheduledOutbox = defineDoc({
|
|
|
8
8
|
checkpointWhen: () => false,
|
|
9
9
|
});
|
|
10
10
|
export function validateSchedules(extensions) {
|
|
11
|
-
const ids = new Set(
|
|
11
|
+
const ids = new Set();
|
|
12
12
|
const requestIds = new Set();
|
|
13
13
|
for (const extension of extensions) {
|
|
14
14
|
for (const schedule of extension.schedules ?? []) {
|
|
@@ -6,28 +6,19 @@ export const Subagent = defineExtension({
|
|
|
6
6
|
tools: [
|
|
7
7
|
defineTool({
|
|
8
8
|
name: "subagent",
|
|
9
|
-
description: "Run a foreground
|
|
9
|
+
description: "Run a foreground subagent with a separate context and the same tools as you. Returns its result here.",
|
|
10
10
|
parameters: Type.Object({ task: Type.String() }),
|
|
11
11
|
replay: "safe",
|
|
12
12
|
async execute(args, api, context) {
|
|
13
|
-
const parent = await api.agent(context);
|
|
14
13
|
const childId = await api.commit(async (tx) => {
|
|
15
14
|
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
16
15
|
if (existing)
|
|
17
16
|
return existing.id;
|
|
18
17
|
const child = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
19
|
-
|
|
20
|
-
"read",
|
|
21
|
-
"search_memory",
|
|
22
|
-
"load_memory",
|
|
23
|
-
"web_search",
|
|
24
|
-
"fetch_content",
|
|
25
|
-
"get_search_results",
|
|
26
|
-
];
|
|
18
|
+
// The child has every tool its parent has, deferred ones included; only nesting another subagent is left out.
|
|
27
19
|
await configure(tx, child.id, {
|
|
28
20
|
extensions: { remove: [Subagent] },
|
|
29
|
-
|
|
30
|
-
instructions: "Complete only the delegated task. You are read-only. Do not send messages or launch background work.",
|
|
21
|
+
instructions: "Complete only the delegated task and return its result; do not take on other work.",
|
|
31
22
|
});
|
|
32
23
|
return child.id;
|
|
33
24
|
}, context);
|
|
@@ -2,7 +2,8 @@ import { readdir, readFile, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join, sep } from "node:path";
|
|
4
4
|
import { Type } from "@earendil-works/pi-ai";
|
|
5
|
-
import
|
|
5
|
+
import * as durable from "@earendil-works/pi-durable";
|
|
6
|
+
import { configure, defineDoc, defineExtension, defineTask, defineTool, GenerationTask, hook, section, } from "@earendil-works/pi-durable";
|
|
6
7
|
import { ScheduledOutbox, validateSchedules, } from "./schedules.js";
|
|
7
8
|
const loadModule = createRequire(import.meta.url);
|
|
8
9
|
/** A chat assistant can apply its own workspace changes through the existing host owner. */
|
|
@@ -22,7 +23,7 @@ export function workspaceExtension(reload) {
|
|
|
22
23
|
],
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
|
-
/** Scan only direct workspace extension directories; never skills or attachments
|
|
26
|
+
/** Scan only direct workspace extension directories; never skills or attachments. */
|
|
26
27
|
export async function loadWorkspaceExtensions(workingDir) {
|
|
27
28
|
const extensions = [];
|
|
28
29
|
const root = join(workingDir, "extensions");
|
|
@@ -69,6 +70,9 @@ export async function loadWorkspaceExtensions(workingDir) {
|
|
|
69
70
|
configure,
|
|
70
71
|
ScheduledOutbox,
|
|
71
72
|
section,
|
|
73
|
+
hook,
|
|
74
|
+
GenerationTask,
|
|
75
|
+
durable,
|
|
72
76
|
});
|
|
73
77
|
if (!extension ||
|
|
74
78
|
typeof extension !== "object" ||
|
package/dist/main.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
2
|
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { createRegistry } from "@earendil-works/pi-durable";
|
|
4
|
+
import { createToolSearchExtension } from "@earendil-works/pi-durable/tool-search";
|
|
4
5
|
import { CodingTools } from "@earendil-works/pi-durable/tools";
|
|
5
6
|
import { Chats, chatConversation, submitMessage, WatchCursor, } from "./agent/chats.js";
|
|
6
7
|
import { compactionReply, isCommand, runCommand } from "./agent/commands.js";
|
|
7
|
-
import { compactChats } from "./agent/compaction.js";
|
|
8
8
|
import { deliverReplies, recoverSending } from "./agent/deliver.js";
|
|
9
9
|
import { deliverDirect } from "./agent/direct-send.js";
|
|
10
|
-
import { logErrors } from "./agent/error-log.js";
|
|
11
10
|
import { openHarness } from "./agent/harness.js";
|
|
12
11
|
import { submitHealth } from "./agent/health.js";
|
|
13
12
|
import { openModels, readDefaults, requestSettings, withModelPolicy } from "./agent/models.js";
|
|
@@ -17,10 +16,10 @@ import { Runs, runRecord } from "./agent/run.js";
|
|
|
17
16
|
import { deliverScheduled, EXECUTION_KIND, Schedules, schedulingExtension } from "./agent/scheduling.js";
|
|
18
17
|
import { isReplyEnabled, readSettings } from "./config/settings.js";
|
|
19
18
|
import { createBrowser } from "./extensions/browser.js";
|
|
19
|
+
import { createCodemode } from "./extensions/codemode.js";
|
|
20
20
|
import { memoryExtension } from "./extensions/memory.js";
|
|
21
21
|
import { messageExtension } from "./extensions/message.js";
|
|
22
22
|
import { ImageRead } from "./extensions/read-image.js";
|
|
23
|
-
import { launchInTerminal, restartExtension } from "./extensions/restart.js";
|
|
24
23
|
import { validateSchedules } from "./extensions/schedules.js";
|
|
25
24
|
import { Subagent } from "./extensions/subagent.js";
|
|
26
25
|
import { webExtension } from "./extensions/web.js";
|
|
@@ -29,6 +28,29 @@ import { archiveAttachments } from "./transport/attachments.js";
|
|
|
29
28
|
import { openHttpTransport } from "./transport/http.js";
|
|
30
29
|
import { createWatcher } from "./transport/watch.js";
|
|
31
30
|
import { startWeb } from "./web/server.js";
|
|
31
|
+
/** Tools offered upfront; every other built-in or workspace tool waits for tool_search. */
|
|
32
|
+
const OFFERED_TOOLS = new Set([
|
|
33
|
+
"read",
|
|
34
|
+
"bash",
|
|
35
|
+
"edit",
|
|
36
|
+
"write",
|
|
37
|
+
"send_message",
|
|
38
|
+
"search_memory",
|
|
39
|
+
"load_memory",
|
|
40
|
+
"save_memory",
|
|
41
|
+
"web_search",
|
|
42
|
+
"fetch_content",
|
|
43
|
+
"tool_search",
|
|
44
|
+
"codemode",
|
|
45
|
+
]);
|
|
46
|
+
function deferRare(extension) {
|
|
47
|
+
if (!extension.tools)
|
|
48
|
+
return extension;
|
|
49
|
+
return {
|
|
50
|
+
...extension,
|
|
51
|
+
tools: extension.tools.map((tool) => OFFERED_TOOLS.has(tool.name) ? tool : { ...tool, deferred: true }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
32
54
|
/** Explicit startup; pass an isolated runtime in tests instead of opening installed auth. */
|
|
33
55
|
export async function startService(options,
|
|
34
56
|
/** The application owns process HTTP resources; standalone callers retain their transport. */
|
|
@@ -46,6 +68,11 @@ reloadHttpTransport) {
|
|
|
46
68
|
}
|
|
47
69
|
const defaults = { ...runtime.defaults, model: { ...runtime.defaults.model } };
|
|
48
70
|
const browser = await createBrowser(options.workingDir, options.agentDir);
|
|
71
|
+
const codemode = createCodemode();
|
|
72
|
+
const closeResources = async () => {
|
|
73
|
+
await codemode.close();
|
|
74
|
+
await browser.close();
|
|
75
|
+
};
|
|
49
76
|
if (!runtime.models.getModel(defaults.model.provider, defaults.model.modelId))
|
|
50
77
|
throw new Error(`Model is unavailable: ${defaults.model.provider}/${defaults.model.modelId}`);
|
|
51
78
|
async function loadExtensions() {
|
|
@@ -72,21 +99,16 @@ reloadHttpTransport) {
|
|
|
72
99
|
: [
|
|
73
100
|
CodingTools,
|
|
74
101
|
ImageRead,
|
|
102
|
+
// Durable's own tool_search loads the deferred tools.
|
|
103
|
+
createToolSearchExtension(),
|
|
75
104
|
await memoryExtension(options.workingDir),
|
|
76
105
|
webExtension(),
|
|
77
106
|
browser.extension,
|
|
107
|
+
codemode.extension,
|
|
78
108
|
Subagent,
|
|
79
109
|
workspaceExtension(reload),
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
workingDir: options.workingDir,
|
|
83
|
-
currentRelease: process.cwd(),
|
|
84
|
-
deployRoot: "/tmp",
|
|
85
|
-
settled: (taskId) => harness.waitForTask(taskId, BACKGROUND_CONTEXT),
|
|
86
|
-
launch: launchInTerminal,
|
|
87
|
-
}),
|
|
88
|
-
]),
|
|
89
|
-
...(await loadWorkspaceExtensions(options.workingDir)),
|
|
110
|
+
].map(deferRare)),
|
|
111
|
+
...(await loadWorkspaceExtensions(options.workingDir)).map(deferRare),
|
|
90
112
|
];
|
|
91
113
|
validateSchedules(extensions);
|
|
92
114
|
const names = new Set();
|
|
@@ -95,7 +117,7 @@ reloadHttpTransport) {
|
|
|
95
117
|
throw new Error(`Duplicate extension name: ${extension.name}`);
|
|
96
118
|
names.add(extension.name);
|
|
97
119
|
}
|
|
98
|
-
//
|
|
120
|
+
// An interrupted tool reruns on recovery unless it declares `unsafe`.
|
|
99
121
|
return extensions.map((extension) => extension.tools
|
|
100
122
|
? { ...extension, tools: extension.tools.map((tool) => ({ ...tool, replay: tool.replay ?? "safe" })) }
|
|
101
123
|
: extension);
|
|
@@ -106,7 +128,6 @@ reloadHttpTransport) {
|
|
|
106
128
|
const { harness, storage } = owner;
|
|
107
129
|
const scheduling = schedulingExtension(harness, defaults);
|
|
108
130
|
let progress;
|
|
109
|
-
const stopErrorLog = logErrors(harness);
|
|
110
131
|
try {
|
|
111
132
|
// The native owner lock is acquired before cleaning up browsers left by a previous process.
|
|
112
133
|
await browser.close();
|
|
@@ -129,7 +150,6 @@ reloadHttpTransport) {
|
|
|
129
150
|
const jobId = input && typeof input === "object" && !Array.isArray(input) ? input.jobId : undefined;
|
|
130
151
|
const saved = savedSchedules?.items.find((schedule) => schedule.id === jobId);
|
|
131
152
|
if (saved &&
|
|
132
|
-
saved.id !== "compact-chats" &&
|
|
133
153
|
!activeExtensions.some((extension) => extension.schedules?.some((schedule) => schedule.id === saved.id)))
|
|
134
154
|
throw new Error(`Missing extension for unfinished schedule: ${saved.id}`);
|
|
135
155
|
}
|
|
@@ -141,7 +161,7 @@ reloadHttpTransport) {
|
|
|
141
161
|
});
|
|
142
162
|
}
|
|
143
163
|
catch (error) {
|
|
144
|
-
await owner.close(
|
|
164
|
+
await owner.close(closeResources);
|
|
145
165
|
throw error;
|
|
146
166
|
}
|
|
147
167
|
let reloading = Promise.resolve();
|
|
@@ -362,24 +382,6 @@ reloadHttpTransport) {
|
|
|
362
382
|
return receipt;
|
|
363
383
|
});
|
|
364
384
|
},
|
|
365
|
-
/** Admit quiet native compactions; observing their results must not hold shutdown open. */
|
|
366
|
-
compact() {
|
|
367
|
-
return track(async () => {
|
|
368
|
-
const tasks = await compactChats(harness);
|
|
369
|
-
for (const task of tasks)
|
|
370
|
-
void harness
|
|
371
|
-
.waitForTask(task, BACKGROUND_CONTEXT)
|
|
372
|
-
.then((settled) => {
|
|
373
|
-
if (settled.state.outcome.status !== "completed")
|
|
374
|
-
console.warn("Scheduled compaction ended", task, settled.state.outcome);
|
|
375
|
-
})
|
|
376
|
-
.catch((error) => {
|
|
377
|
-
if (!closed)
|
|
378
|
-
console.warn("Scheduled compaction observation failed", task, error);
|
|
379
|
-
});
|
|
380
|
-
return tasks;
|
|
381
|
-
});
|
|
382
|
-
},
|
|
383
385
|
// Explicit host boundary; no model loop, timer or transport runs on import.
|
|
384
386
|
deliver() {
|
|
385
387
|
return track(async () => {
|
|
@@ -396,8 +398,7 @@ reloadHttpTransport) {
|
|
|
396
398
|
closing ??= (async () => {
|
|
397
399
|
await Promise.allSettled([...operations]);
|
|
398
400
|
await progress.close();
|
|
399
|
-
|
|
400
|
-
await owner.close(browser.close);
|
|
401
|
+
await owner.close(closeResources);
|
|
401
402
|
})();
|
|
402
403
|
return closing;
|
|
403
404
|
},
|
|
@@ -449,15 +450,10 @@ export async function startMessaging(options, reloadHttpTransport) {
|
|
|
449
450
|
let closing;
|
|
450
451
|
let timer;
|
|
451
452
|
let started = false;
|
|
452
|
-
/** Rows committed before the call are read even when a poll is already in flight. */
|
|
453
453
|
function poll() {
|
|
454
454
|
if (closed)
|
|
455
455
|
return Promise.reject(new Error("Messaging is closed"));
|
|
456
|
-
|
|
457
|
-
const current = previous
|
|
458
|
-
.then(async () => {
|
|
459
|
-
if (closed)
|
|
460
|
-
return;
|
|
456
|
+
pending ??= (async () => {
|
|
461
457
|
try {
|
|
462
458
|
await watcher.poll();
|
|
463
459
|
}
|
|
@@ -466,13 +462,10 @@ export async function startMessaging(options, reloadHttpTransport) {
|
|
|
466
462
|
if (!closed)
|
|
467
463
|
await service.deliver();
|
|
468
464
|
}
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
if (pending === current)
|
|
472
|
-
pending = undefined;
|
|
465
|
+
})().finally(() => {
|
|
466
|
+
pending = undefined;
|
|
473
467
|
});
|
|
474
|
-
pending
|
|
475
|
-
return current;
|
|
468
|
+
return pending;
|
|
476
469
|
}
|
|
477
470
|
function tick() {
|
|
478
471
|
void poll()
|
package/dist/transport/watch.js
CHANGED
|
@@ -62,7 +62,13 @@ export function createWatcher(options) {
|
|
|
62
62
|
const attachments = db
|
|
63
63
|
.prepare(`SELECT a.filename FROM attachment a
|
|
64
64
|
JOIN message_attachment_join j ON j.attachment_id=a.ROWID WHERE j.message_id=?`)
|
|
65
|
-
.all(row.rowid).flatMap(({ filename }) =>
|
|
65
|
+
.all(row.rowid).flatMap(({ filename }) =>
|
|
66
|
+
// Link-preview and iMessage app payload plists carry nothing a model can read; the text already
|
|
67
|
+
// holds the URL. Messages may record one in a transient transcoder directory and delete the
|
|
68
|
+
// file, so archiving it would hold this row forever.
|
|
69
|
+
filename && !filename.endsWith(".pluginPayloadAttachment")
|
|
70
|
+
? [filename.replace(/^~(?=\/)/, homedir())]
|
|
71
|
+
: []);
|
|
66
72
|
const reply = row.replyGuid
|
|
67
73
|
? db.prepare("SELECT text,attributedBody FROM message WHERE guid=?").get(row.replyGuid)
|
|
68
74
|
: undefined;
|
package/dist/web/app.js
CHANGED
|
@@ -427,15 +427,18 @@ function renderSettings() {
|
|
|
427
427
|
|
|
428
428
|
function renderChatSettings() {
|
|
429
429
|
const chats = data.chats || [];
|
|
430
|
-
const overrides = chats.filter(
|
|
430
|
+
const overrides = chats.filter(
|
|
431
|
+
(chat) => chat.verbose.chat !== undefined || chat.interim.chat !== undefined,
|
|
432
|
+
).length;
|
|
431
433
|
view.replaceChildren(
|
|
432
434
|
element(
|
|
433
435
|
"p",
|
|
434
436
|
[
|
|
435
437
|
`${chats.length} chats`,
|
|
436
438
|
`default verbose ${data.defaults?.progressMessages ? "on" : "off"}`,
|
|
439
|
+
`default interim ${data.defaults?.interimMessages ? "on" : "off"}`,
|
|
437
440
|
`${overrides} ${overrides === 1 ? "override" : "overrides"}`,
|
|
438
|
-
"read-only: /verbose and /thinking change a chat, settings.json the default",
|
|
441
|
+
"read-only: /verbose, /interim and /thinking change a chat, settings.json the default",
|
|
439
442
|
].join(" · "),
|
|
440
443
|
"muted",
|
|
441
444
|
),
|
|
@@ -455,7 +458,7 @@ function renderChatSettings() {
|
|
|
455
458
|
card.append(header);
|
|
456
459
|
const table = element("table", undefined, "chat-table");
|
|
457
460
|
const heading = table.createTHead().insertRow();
|
|
458
|
-
for (const label of ["Chat", "Replies", "Verbose", "Model", "Thinking", "Last activity"])
|
|
461
|
+
for (const label of ["Chat", "Replies", "Verbose", "Interim", "Model", "Thinking", "Last activity"])
|
|
459
462
|
heading.append(element("th", label));
|
|
460
463
|
const body = table.createTBody();
|
|
461
464
|
for (const chat of rows) {
|
|
@@ -476,6 +479,10 @@ function renderChatSettings() {
|
|
|
476
479
|
chat.verbose.effective ? "on" : "off",
|
|
477
480
|
chat.verbose.chat === undefined ? "default" : "this chat",
|
|
478
481
|
),
|
|
482
|
+
stacked(
|
|
483
|
+
chat.interim.effective ? "on" : "off",
|
|
484
|
+
chat.interim.chat === undefined ? "default" : "this chat",
|
|
485
|
+
),
|
|
479
486
|
stacked(modelId.join("/") || "-", chat.model ? provider : undefined),
|
|
480
487
|
stacked(chat.thinking || "-"),
|
|
481
488
|
stacked(activity || "-"),
|
|
@@ -615,6 +622,7 @@ function renderScheduled() {
|
|
|
615
622
|
|
|
616
623
|
function renderTasks() {
|
|
617
624
|
view.replaceChildren();
|
|
625
|
+
const children = new Map();
|
|
618
626
|
const conversations = new Map(data.conversations.map((conversation) => [conversation.id, conversation]));
|
|
619
627
|
const chatNames = new Map(
|
|
620
628
|
(data.chats?.items || []).map((chat) => [
|
|
@@ -622,130 +630,70 @@ function renderTasks() {
|
|
|
622
630
|
chatName({ ...chat, history: { groupName: conversations.get(chat.conversationId)?.groupName } }, []),
|
|
623
631
|
]),
|
|
624
632
|
);
|
|
633
|
+
const conversationNames = new Map(chatNames);
|
|
625
634
|
const scheduleNames = new Map((data.schedules?.items || []).map((job) => [job.conversationId, job.name]));
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (sessionNames.has(id)) return { name: `session ${sessionNames.get(id)}`, doc: "imessage.sessions" };
|
|
635
|
+
for (const [id, name] of scheduleNames) conversationNames.set(id, name);
|
|
636
|
+
for (const session of data.sessions?.items || []) {
|
|
637
|
+
if (!conversationNames.has(session.conversationId))
|
|
638
|
+
conversationNames.set(session.conversationId, session.label);
|
|
639
|
+
}
|
|
640
|
+
function conversationName(id) {
|
|
641
|
+
const name = conversationNames.get(id);
|
|
642
|
+
if (name) return name;
|
|
635
643
|
const owner = conversations.get(id)?.owner;
|
|
636
|
-
|
|
644
|
+
if (!owner) return "";
|
|
645
|
+
const parentName = conversationName(owner.conversationId);
|
|
646
|
+
return parentName ? `${parentName} / subagent` : "subagent";
|
|
637
647
|
}
|
|
638
648
|
function belongsToChat(id) {
|
|
639
649
|
if (chatNames.has(id) || scheduleNames.has(id)) return true;
|
|
640
650
|
const owner = conversations.get(id)?.owner;
|
|
641
651
|
return owner ? belongsToChat(owner.conversationId) : false;
|
|
642
652
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
// later root tasks and loose entries of its conversation nest under it until the next one.
|
|
646
|
-
const children = new Map();
|
|
647
|
-
const add = (key, node) => {
|
|
653
|
+
for (const conversation of data.conversations.toSorted(compareConversations)) {
|
|
654
|
+
const key = conversation.owner ? `task-${conversation.owner.taskId}` : "root";
|
|
648
655
|
if (!children.has(key)) children.set(key, []);
|
|
649
|
-
children.get(key).push(
|
|
650
|
-
};
|
|
651
|
-
const taskIds = new Set(data.tasks.map((task) => task.id));
|
|
652
|
-
for (const conversation of data.conversations) {
|
|
653
|
-
if (conversation.owner)
|
|
654
|
-
add(`task-${conversation.owner.taskId}`, { type: "conversation", ...conversation });
|
|
655
|
-
}
|
|
656
|
-
const turns = new Map();
|
|
657
|
-
for (const entry of data.entries || []) {
|
|
658
|
-
if (entry.kind !== "pi.user") continue;
|
|
659
|
-
if (!turns.has(entry.conversationId)) turns.set(entry.conversationId, []);
|
|
660
|
-
turns.get(entry.conversationId).push(entry.id);
|
|
661
|
-
}
|
|
662
|
-
// Entries arrive newest first; a turn lookup needs ascending ids.
|
|
663
|
-
for (const ids of turns.values()) ids.sort((left, right) => left - right);
|
|
664
|
-
function turnKey(conversationId, id) {
|
|
665
|
-
const opened = (turns.get(conversationId) || []).filter((turn) => turn < id).at(-1);
|
|
666
|
-
return opened === undefined ? `conversation-${conversationId}` : `entry-${opened}`;
|
|
656
|
+
children.get(key).push({ type: "conversation", ...conversation });
|
|
667
657
|
}
|
|
668
658
|
for (const task of data.tasks) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
for (const entry of data.entries || []) {
|
|
675
|
-
if (entry.kind === "pi.user") add(`conversation-${entry.conversationId}`, { type: "entry", ...entry });
|
|
676
|
-
else if (entry.byTaskId !== undefined && taskIds.has(entry.byTaskId))
|
|
677
|
-
add(`task-${entry.byTaskId}`, { type: "entry", ...entry });
|
|
678
|
-
else add(turnKey(entry.conversationId, entry.id), { type: "entry", ...entry });
|
|
679
|
-
}
|
|
680
|
-
// Newest turn first in a card; inside a turn the chain reads in storage order. Cards are sorted by activity.
|
|
681
|
-
for (const [key, nodes] of children) {
|
|
682
|
-
if (key.startsWith("conversation-")) nodes.sort((left, right) => right.id - left.id);
|
|
683
|
-
else nodes.sort((left, right) => left.id - right.id);
|
|
684
|
-
}
|
|
685
|
-
function lazyDetails(summary, url) {
|
|
686
|
-
const detail = element("details");
|
|
687
|
-
const content = element("pre", "Loading latest state...");
|
|
688
|
-
detail.append(summary, content);
|
|
689
|
-
let loaded = false;
|
|
690
|
-
detail.addEventListener("toggle", async () => {
|
|
691
|
-
if (!detail.open || loaded) return;
|
|
692
|
-
loaded = true;
|
|
693
|
-
try {
|
|
694
|
-
content.textContent = JSON.stringify(await fetchData(url), null, 2);
|
|
695
|
-
} catch (error) {
|
|
696
|
-
loaded = false;
|
|
697
|
-
content.textContent = error.message;
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
return detail;
|
|
701
|
-
}
|
|
702
|
-
function taskSummary(node) {
|
|
703
|
-
const label =
|
|
704
|
-
node.kind === "pi.generation"
|
|
705
|
-
? "Generation"
|
|
706
|
-
: node.kind === "pi.tool"
|
|
707
|
-
? `Tool ${node.name || "?"}`
|
|
708
|
-
: node.kind;
|
|
709
|
-
const summary = element("summary", label);
|
|
710
|
-
const state = node.outcome ? [node.outcome] : [node.status, node.phase].filter(Boolean);
|
|
711
|
-
const waiting = node.on?.length ? ` on ${node.on.length} task${node.on.length === 1 ? "" : "s"}` : "";
|
|
712
|
-
const sleeping =
|
|
713
|
-
typeof node.until === "number" ? ` until ${new Date(node.until).toLocaleString("sv-SE")}` : "";
|
|
714
|
-
const flags = `${node.background ? " [background]" : ""}${node.abortRequested ? " [abort requested]" : ""}`;
|
|
715
|
-
summary.append(element("span", ` [${state.join(" / ")}]${waiting}${sleeping}${flags}`, "meta"));
|
|
716
|
-
return summary;
|
|
717
|
-
}
|
|
718
|
-
function entrySummary(node) {
|
|
719
|
-
const summary = element("summary", node.kind.replace(/^pi\./, ""));
|
|
720
|
-
if (node.text) summary.append(element("span", ` "${node.text}"`, "entry-text"));
|
|
721
|
-
return summary;
|
|
659
|
+
const key = task.owner === undefined ? `conversation-${task.conversationId}` : `task-${task.owner}`;
|
|
660
|
+
if (!children.has(key)) children.set(key, []);
|
|
661
|
+
children.get(key).push({ type: "task", ...task });
|
|
722
662
|
}
|
|
723
663
|
function renderNode(node) {
|
|
724
664
|
const item = element("li");
|
|
725
665
|
const key = `${node.type}-${node.id}`;
|
|
726
666
|
item.id = `tree-${key}`;
|
|
727
667
|
if (node.type === "conversation") {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (node.owner) {
|
|
736
|
-
const detail = element("details");
|
|
737
|
-
detail.id = key;
|
|
738
|
-
detail.append(heading);
|
|
739
|
-
item.append(detail);
|
|
740
|
-
} else item.append(heading);
|
|
741
|
-
} else if (node.type === "task") {
|
|
742
|
-
const detail = lazyDetails(taskSummary(node), `/tasks/task/data?taskId=${node.id}`);
|
|
743
|
-
detail.id = key;
|
|
744
|
-
item.append(detail);
|
|
668
|
+
const heading = element("span", `Conversation #${node.id}`);
|
|
669
|
+
const name = conversationName(node.id);
|
|
670
|
+
if (name) heading.append(element("span", ` / ${name} `, "meta"));
|
|
671
|
+
if (chatNames.has(node.id)) {
|
|
672
|
+
heading.append(link("Chat", `/chat?conversationId=${node.id}`, "meta"));
|
|
673
|
+
}
|
|
674
|
+
item.append(heading);
|
|
745
675
|
} else {
|
|
746
|
-
|
|
747
|
-
const detail = lazyDetails(entrySummary(node), `/tasks/entry/data?entryId=${node.id}`);
|
|
676
|
+
const detail = element("details");
|
|
748
677
|
detail.id = key;
|
|
678
|
+
const summary = element("summary", `${node.kind}${node.name ? `: ${node.name}` : ""} #${node.id}`);
|
|
679
|
+
const status = [node.status, node.phase, node.outcome].filter(Boolean).join(" / ");
|
|
680
|
+
const waiting = node.on?.length ? ` on [${node.on.join(", ")}]` : "";
|
|
681
|
+
const flags = `${node.background ? " [background]" : ""}${node.abortRequested ? " [abort requested]" : ""}`;
|
|
682
|
+
summary.append(element("span", ` ${status}${waiting}${flags}`, "meta"));
|
|
683
|
+
const content = element("pre", "Loading latest state...");
|
|
684
|
+
detail.append(summary, content);
|
|
685
|
+
let loaded = false;
|
|
686
|
+
detail.addEventListener("toggle", async () => {
|
|
687
|
+
if (!detail.open || loaded) return;
|
|
688
|
+
loaded = true;
|
|
689
|
+
try {
|
|
690
|
+
const task = await fetchData(`/tasks/task/data?taskId=${node.id}`);
|
|
691
|
+
content.textContent = JSON.stringify(task, null, 2);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
loaded = false;
|
|
694
|
+
content.textContent = error.message;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
749
697
|
item.append(detail);
|
|
750
698
|
}
|
|
751
699
|
const descendants = children.get(key) || [];
|
|
@@ -753,29 +701,23 @@ function renderTasks() {
|
|
|
753
701
|
if (descendants.length || more) {
|
|
754
702
|
const list = element("ul", undefined, "task-tree");
|
|
755
703
|
for (const child of descendants) list.append(renderNode(child));
|
|
756
|
-
if (more) list.append(element("li", "older
|
|
757
|
-
|
|
704
|
+
if (more) list.append(element("li", "older tasks not listed", "meta"));
|
|
705
|
+
item.append(list);
|
|
758
706
|
}
|
|
759
707
|
return item;
|
|
760
708
|
}
|
|
761
709
|
const roots = conversationId
|
|
762
|
-
? data.conversations
|
|
763
|
-
|
|
710
|
+
? data.conversations
|
|
711
|
+
.filter((conversation) => String(conversation.id) === conversationId)
|
|
712
|
+
.map((conversation) => ({ type: "conversation", ...conversation }))
|
|
713
|
+
: children.get("root") || [];
|
|
764
714
|
const visible = roots.filter((conversation) => belongsToChat(conversation.id));
|
|
765
|
-
if (!conversationId && data.session)
|
|
766
|
-
view.append(
|
|
767
|
-
element(
|
|
768
|
-
"p",
|
|
769
|
-
`Session ${data.session} docs: imessage.chats, imessage.schedules, imessage.sessions`,
|
|
770
|
-
"muted",
|
|
771
|
-
),
|
|
772
|
-
);
|
|
773
715
|
if (!visible.length) view.append(element("p", "No conversations.", "empty"));
|
|
774
716
|
for (const conversation of visible.toSorted(compareConversations)) {
|
|
775
717
|
const card = element("section", undefined, "card");
|
|
776
718
|
card.id = `tasks-${conversation.id}`;
|
|
777
719
|
const list = element("ul", undefined, "task-tree task-root");
|
|
778
|
-
list.append(renderNode(
|
|
720
|
+
list.append(renderNode(conversation));
|
|
779
721
|
card.append(list);
|
|
780
722
|
view.append(card);
|
|
781
723
|
}
|
package/dist/web/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { gzip } from "node:zlib";
|
|
7
7
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
8
8
|
import { LiveDoc, } from "@earendil-works/pi-durable";
|
|
9
|
-
import { Chats, progressEnabled } from "../agent/chats.js";
|
|
9
|
+
import { Chats, interimEnabled, progressEnabled } from "../agent/chats.js";
|
|
10
10
|
import { DirectSends } from "../agent/direct-send.js";
|
|
11
11
|
import { Sessions } from "../agent/health.js";
|
|
12
12
|
import { Deliveries } from "../agent/replies.js";
|
|
@@ -113,7 +113,7 @@ export async function startWeb(options) {
|
|
|
113
113
|
settings: { chatAllowlist: configuration.chatAllowlist },
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
-
/** One row per chat: reply eligibility, the /verbose
|
|
116
|
+
/** One row per chat: reply eligibility, the /verbose and /interim choices and the conversation's agent settings. */
|
|
117
117
|
async function chatSettings() {
|
|
118
118
|
const [chats, configuration] = await Promise.all([
|
|
119
119
|
options.agent.harness.snapshot(Chats, BACKGROUND_CONTEXT),
|
|
@@ -132,6 +132,7 @@ export async function startWeb(options) {
|
|
|
132
132
|
updatedAt: entries ? historyActivity(entries) : undefined,
|
|
133
133
|
replies: isReplyEnabled(configuration, chat.chatGuid),
|
|
134
134
|
verbose: { chat: chat.progress, effective: progressEnabled(configuration, records, chat.chatGuid) },
|
|
135
|
+
interim: { chat: chat.interim, effective: interimEnabled(configuration, records, chat.chatGuid) },
|
|
135
136
|
model: agent?.model ? `${agent.model.provider}/${agent.model.modelId}` : undefined,
|
|
136
137
|
thinking: agent?.thinkingLevel,
|
|
137
138
|
};
|
|
@@ -139,7 +140,10 @@ export async function startWeb(options) {
|
|
|
139
140
|
rows.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0) || Number(b.conversationId) - Number(a.conversationId));
|
|
140
141
|
return {
|
|
141
142
|
chats: rows,
|
|
142
|
-
defaults: {
|
|
143
|
+
defaults: {
|
|
144
|
+
progressMessages: configuration.progressMessages,
|
|
145
|
+
interimMessages: configuration.interimMessages,
|
|
146
|
+
},
|
|
143
147
|
revision: viewRevision(),
|
|
144
148
|
};
|
|
145
149
|
}
|
|
@@ -281,21 +285,10 @@ export async function startWeb(options) {
|
|
|
281
285
|
schedules: await options.agent.harness.snapshot(Schedules, BACKGROUND_CONTEXT),
|
|
282
286
|
chats: await options.agent.harness.snapshot(Chats, BACKGROUND_CONTEXT),
|
|
283
287
|
sessions: await options.agent.harness.snapshot(Sessions, BACKGROUND_CONTEXT),
|
|
284
|
-
session: join(options.workingDir, "durable"),
|
|
285
288
|
revision: viewRevision(),
|
|
286
289
|
};
|
|
287
290
|
compact = true;
|
|
288
291
|
}
|
|
289
|
-
else if (url.pathname === "/tasks/entry/data") {
|
|
290
|
-
const id = url.searchParams.get("entryId");
|
|
291
|
-
if (!id || !/^\d+$/.test(id) || !Number.isSafeInteger(Number(id)))
|
|
292
|
-
throw new Error("Invalid entry ID");
|
|
293
|
-
value = await options.agent.harness.commit((tx) => tx.entry(Number(id)), BACKGROUND_CONTEXT);
|
|
294
|
-
if (!value) {
|
|
295
|
-
await json(response, 404, { error: "Entry not found" });
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
292
|
else if (url.pathname === "/tasks/task/data") {
|
|
300
293
|
const id = url.searchParams.get("taskId");
|
|
301
294
|
if (!id || !/^\d+$/.test(id) || !Number.isSafeInteger(Number(id)))
|
package/dist/web/style.css
CHANGED
|
@@ -439,12 +439,6 @@ body.page-logs .native-state {
|
|
|
439
439
|
padding: 8px 0 8px 16px;
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
-
/* Entries are history records, not work: all grey, no marker of their own. */
|
|
443
|
-
.task-tree li.entry > details > summary {
|
|
444
|
-
opacity: 0.6;
|
|
445
|
-
list-style: none;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
442
|
.schedule-row {
|
|
449
443
|
background: var(--surface);
|
|
450
444
|
margin-bottom: 6px;
|