@kingcrab/pi-imessage 0.0.43 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -127
- package/dist/agent/behavior.js +79 -0
- package/dist/agent/chats.js +88 -0
- package/dist/agent/commands.js +191 -0
- package/dist/agent/compaction.js +51 -0
- package/dist/agent/deliver.js +139 -0
- package/dist/agent/direct-send.js +69 -0
- package/dist/agent/harness.js +79 -0
- package/dist/agent/health.js +33 -0
- package/dist/agent/models.js +69 -0
- package/dist/agent/progress-format.js +87 -0
- package/dist/agent/progress.js +173 -0
- package/dist/agent/prompt.js +79 -0
- package/dist/agent/replies.js +10 -0
- package/dist/agent/run.js +160 -0
- package/dist/agent/scheduling.js +291 -0
- package/dist/agent/time.js +18 -0
- package/dist/cli.js +88 -191
- package/dist/config/settings.js +60 -0
- package/dist/extensions/browser.js +326 -0
- package/dist/extensions/final-text.js +23 -0
- package/dist/extensions/memory.js +109 -0
- package/dist/extensions/message.js +33 -0
- package/dist/extensions/read-image.js +72 -0
- package/dist/extensions/restart.js +100 -0
- package/dist/extensions/schedules.js +42 -0
- package/dist/extensions/subagent.js +54 -0
- package/dist/extensions/web.js +100 -0
- package/dist/extensions/workspace.js +98 -0
- package/dist/main.js +578 -88
- package/dist/transport/attachments.js +37 -0
- package/dist/transport/http.js +110 -0
- package/dist/{rich-text.js → transport/rich-text.js} +25 -13
- package/dist/{send.js → transport/send.js} +13 -13
- package/dist/transport/watch.js +124 -0
- package/dist/web/app.js +826 -0
- package/dist/web/history.js +87 -0
- package/dist/web/memory.js +81 -0
- package/dist/web/page.js +29 -0
- package/dist/web/schedules.js +63 -0
- package/dist/web/server.js +382 -0
- package/dist/web/style.css +577 -0
- package/dist/web/tasks.js +97 -0
- package/docs/adr/0001-durable-owns-state.md +11 -0
- package/docs/adr/0002-module-layering.md +11 -0
- package/docs/adr/0003-replies-at-most-once.md +11 -0
- package/docs/adr/0004-native-extensions.md +11 -0
- package/docs/adr/0005-history-migration.md +11 -0
- package/docs/adr/0006-jsonl-storage.md +11 -0
- package/docs/adr/0007-images-by-path.md +11 -0
- package/docs/adr/0008-compaction-and-run.md +11 -0
- package/docs/adr/0009-one-shot-cutover.md +11 -0
- package/docs/adr/0010-test-budget.md +11 -0
- package/docs/adr/0011-reconcile-unsent-answers.md +24 -0
- package/docs/adr/0012-run-in-owned-conversation.md +22 -0
- package/docs/adr/0013-thin-adapters-around-durable.md +18 -0
- package/docs/adr/0014-ephemeral-private-storage.md +13 -0
- package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +21 -0
- package/docs/adr/0016-process-http-transport.md +21 -0
- package/docs/adr/0017-browser-cli-owned-scope.md +22 -0
- package/docs/adr/0018-tasks-without-prompt-api.md +19 -0
- package/docs/adr/0019-remove-scheduled-services.md +41 -0
- package/docs/adr/0020-chat-task-progress.md +41 -0
- package/docs/adr/0021-readable-progress-without-link-previews.md +33 -0
- package/docs/adr/0022-progress-admission-and-settlement.md +35 -0
- package/docs/adr/0023-retain-progress-type-prefixes.md +18 -0
- package/docs/adr/0024-status-marker-before-type-prefix.md +13 -0
- package/docs/adr/0025-reusable-progress-and-command-preview.md +49 -0
- package/docs/adr/0026-inline-command-only-requests.md +16 -0
- package/docs/adr/0027-command-preview-width.md +10 -0
- package/docs/adr/0028-command-preview-120.md +8 -0
- package/docs/adr/0030-shared-web-read-cache.md +33 -0
- package/docs/adr/0031-quiet-compaction-progress.md +32 -0
- package/docs/adr/0032-native-background-schedules.md +58 -0
- package/docs/adr/0033-operator-scheduled-run.md +23 -0
- package/docs/adr/0034-machine-local-schedule-time.md +36 -0
- package/docs/adr/0035-external-business-task-extensions.md +56 -0
- package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +37 -0
- package/docs/adr/0037-restart-during-in-flight-work.md +30 -0
- package/docs/adr/0038-restart-any-time.md +25 -0
- package/docs/adr/0039-rerun-interrupted-tools.md +24 -0
- package/docs/adr/0040-chat-progress-notices.md +44 -0
- package/docs/adr/0041-restart-and-recovery.md +28 -0
- package/docs/adr/0042-background-schedules-and-workspace-extensions.md +44 -0
- package/docs/adr/0043-remove-legacy-migration.md +23 -0
- package/docs/adr/0044-message-channels.md +32 -0
- package/docs/adr/0045-chat-scheduling-tools.md +51 -0
- package/docs/adr/0046-selective-command-redaction.md +31 -0
- package/docs/adr/0047-one-scheduling-path.md +158 -0
- package/docs/adr/0048-quiet-attachment-reads.md +11 -0
- package/docs/adr/0049-per-chat-progress-notices.md +21 -0
- package/docs/adr/0052-host-owned-self-restart.md +53 -0
- package/docs/adr/README.md +54 -0
- package/docs/api.md +104 -0
- package/docs/architecture.md +164 -0
- package/docs/browser.md +93 -0
- package/docs/memory.md +27 -0
- package/docs/screenshot.png +0 -0
- package/docs/workspace-extensions.md +133 -0
- package/examples/workspace-extensions/system-context/index.ts +69 -0
- package/examples/workspace-extensions/system-context/package.json +3 -0
- package/ops/README.md +250 -0
- package/ops/preflight.mjs +28 -0
- package/package.json +36 -37
- package/dist/agent.d.ts +0 -84
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js +0 -780
- package/dist/agent.js.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/imessage.d.ts +0 -33
- package/dist/imessage.d.ts.map +0 -1
- package/dist/imessage.js +0 -76
- package/dist/imessage.js.map +0 -1
- package/dist/logger.d.ts +0 -33
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -104
- package/dist/logger.js.map +0 -1
- package/dist/main.d.ts +0 -5
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/memory.d.ts +0 -53
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -196
- package/dist/memory.js.map +0 -1
- package/dist/model-health.d.ts +0 -11
- package/dist/model-health.d.ts.map +0 -1
- package/dist/model-health.js +0 -69
- package/dist/model-health.js.map +0 -1
- package/dist/pipeline.d.ts +0 -27
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/pipeline.js +0 -59
- package/dist/pipeline.js.map +0 -1
- package/dist/queue.d.ts +0 -23
- package/dist/queue.d.ts.map +0 -1
- package/dist/queue.js +0 -89
- package/dist/queue.js.map +0 -1
- package/dist/reminders.d.ts +0 -42
- package/dist/reminders.d.ts.map +0 -1
- package/dist/reminders.js +0 -207
- package/dist/reminders.js.map +0 -1
- package/dist/rich-text.d.ts +0 -26
- package/dist/rich-text.d.ts.map +0 -1
- package/dist/rich-text.js.map +0 -1
- package/dist/self-echo.d.ts +0 -19
- package/dist/self-echo.d.ts.map +0 -1
- package/dist/self-echo.js +0 -54
- package/dist/self-echo.js.map +0 -1
- package/dist/send.d.ts +0 -24
- package/dist/send.d.ts.map +0 -1
- package/dist/send.js.map +0 -1
- package/dist/settings.d.ts +0 -44
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -80
- package/dist/settings.js.map +0 -1
- package/dist/store.d.ts +0 -57
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -124
- package/dist/store.js.map +0 -1
- package/dist/tasks.d.ts +0 -95
- package/dist/tasks.d.ts.map +0 -1
- package/dist/tasks.js +0 -384
- package/dist/tasks.js.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -38
- package/dist/types.js.map +0 -1
- package/dist/watch.d.ts +0 -21
- package/dist/watch.d.ts.map +0 -1
- package/dist/watch.js +0 -219
- package/dist/watch.js.map +0 -1
- package/dist/web/data.d.ts +0 -11
- package/dist/web/data.d.ts.map +0 -1
- package/dist/web/data.js +0 -56
- package/dist/web/data.js.map +0 -1
- package/dist/web/html.d.ts +0 -5
- package/dist/web/html.d.ts.map +0 -1
- package/dist/web/html.js +0 -16
- package/dist/web/html.js.map +0 -1
- package/dist/web/index.d.ts +0 -25
- package/dist/web/index.d.ts.map +0 -1
- package/dist/web/index.js +0 -335
- package/dist/web/index.js.map +0 -1
- package/dist/web/render.d.ts +0 -11
- package/dist/web/render.d.ts.map +0 -1
- package/dist/web/render.js +0 -56
- package/dist/web/render.js.map +0 -1
- package/dist/web/templates/logs.eta +0 -75
- package/dist/web/templates/memory.eta +0 -91
- package/dist/web/templates/page.eta +0 -98
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
6
|
+
import { isReplyEnabled, readSettings } from "../config/settings.js";
|
|
7
|
+
import { Chats, progressEnabled } from "./chats.js";
|
|
8
|
+
import { takeScheduledCompaction } from "./compaction.js";
|
|
9
|
+
import { deliverDirect } from "./direct-send.js";
|
|
10
|
+
import { argumentSummary, commandPreview, formatProgress } from "./progress-format.js";
|
|
11
|
+
import { Runs } from "./run.js";
|
|
12
|
+
async function calledTool(harness, task) {
|
|
13
|
+
if (task.kind !== "pi.tool")
|
|
14
|
+
return;
|
|
15
|
+
// The registered native task owns this input shape and its branded assistant ID.
|
|
16
|
+
const input = task.input;
|
|
17
|
+
// Nested calls belong to a parent tool; only model-issued calls get progress notices.
|
|
18
|
+
if (input.kind !== "model")
|
|
19
|
+
return;
|
|
20
|
+
const entry = await harness.commit((tx) => tx.entry(input.assistant), BACKGROUND_CONTEXT);
|
|
21
|
+
const message = entry?.model?.[0];
|
|
22
|
+
if (message?.role !== "assistant")
|
|
23
|
+
return;
|
|
24
|
+
const call = message.content.find((part) => part.type === "toolCall" && part.id === input.callId);
|
|
25
|
+
return call?.type === "toolCall" ? call : undefined;
|
|
26
|
+
}
|
|
27
|
+
async function canonicalPath(path) {
|
|
28
|
+
return realpath(path).catch(async () => resolve(await realpath(dirname(path)).catch(() => dirname(path)), basename(path)));
|
|
29
|
+
}
|
|
30
|
+
/** Classify metadata only; attachment reads still execute and keep their normal results/history. */
|
|
31
|
+
async function attachmentPath(workingDir, path) {
|
|
32
|
+
if (typeof path !== "string")
|
|
33
|
+
return false;
|
|
34
|
+
// Match the native read tool's @, Unicode-space and home-path normalization.
|
|
35
|
+
let normalized = path.replace(/[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g, " ");
|
|
36
|
+
if (normalized.startsWith("@"))
|
|
37
|
+
normalized = normalized.slice(1);
|
|
38
|
+
if (normalized === "~" || normalized.startsWith("~/"))
|
|
39
|
+
normalized = resolve(homedir(), normalized.slice(2));
|
|
40
|
+
if (normalized.startsWith("file://")) {
|
|
41
|
+
try {
|
|
42
|
+
normalized = fileURLToPath(normalized);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* Keep malformed URLs as ordinary paths, like native read. */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const root = resolve(workingDir, "attachments");
|
|
49
|
+
const candidate = resolve(workingDir, normalized);
|
|
50
|
+
const roots = [root, await canonicalPath(root)];
|
|
51
|
+
const candidates = [candidate, await canonicalPath(candidate)];
|
|
52
|
+
return roots.some((directory) => candidates.some((file) => file === directory || file.startsWith(`${directory}${sep}`)));
|
|
53
|
+
}
|
|
54
|
+
/** Observe native admission and settlement; Durable owns execution and recovery. */
|
|
55
|
+
export async function startProgress(harness, workingDir, sender) {
|
|
56
|
+
// Existing live work belongs to the previous startup; do not replay historical progress.
|
|
57
|
+
const seen = new Set((await harness.inspect(BACKGROUND_CONTEXT)).tasks.map(({ record }) => record.id));
|
|
58
|
+
const active = new Map();
|
|
59
|
+
let pending = Promise.resolve();
|
|
60
|
+
async function notify(task, notice) {
|
|
61
|
+
if (task.kind === "pi.compaction" && (await takeScheduledCompaction(harness, task.id)))
|
|
62
|
+
return;
|
|
63
|
+
const settings = await readSettings(workingDir);
|
|
64
|
+
const chats = (await harness.snapshot(Chats, BACKGROUND_CONTEXT))?.items ?? [];
|
|
65
|
+
// Nothing can be enabled when the global setting is off and no chat chose /verbose on.
|
|
66
|
+
if (!settings.progressMessages && !chats.some((chat) => chat.progress))
|
|
67
|
+
return;
|
|
68
|
+
const runs = (await harness.snapshot(Runs, BACKGROUND_CONTEXT))?.items ?? [];
|
|
69
|
+
let conversationId = task.conversationId;
|
|
70
|
+
let chatGuid;
|
|
71
|
+
const parents = [];
|
|
72
|
+
while (!chatGuid) {
|
|
73
|
+
chatGuid = [...chats, ...runs].find((item) => item.conversationId === conversationId)?.chatGuid;
|
|
74
|
+
if (chatGuid)
|
|
75
|
+
break;
|
|
76
|
+
const conversation = await harness.commit((tx) => tx.conversation(conversationId), BACKGROUND_CONTEXT);
|
|
77
|
+
if (!conversation?.owner)
|
|
78
|
+
return; // Health and unrelated ownerless conversations stay local.
|
|
79
|
+
const owner = await harness.getTask(conversation.owner.taskId, BACKGROUND_CONTEXT);
|
|
80
|
+
const call = owner && (await calledTool(harness, owner));
|
|
81
|
+
parents.unshift(call?.name ?? owner?.kind ?? "child");
|
|
82
|
+
conversationId = conversation.owner.conversationId;
|
|
83
|
+
}
|
|
84
|
+
if (!isReplyEnabled(settings, chatGuid) || !progressEnabled(settings, chats, chatGuid))
|
|
85
|
+
return;
|
|
86
|
+
const tool = await calledTool(harness, task);
|
|
87
|
+
if (tool?.name === "read" && (await attachmentPath(workingDir, tool.arguments.path)))
|
|
88
|
+
return;
|
|
89
|
+
const request = tool
|
|
90
|
+
? {
|
|
91
|
+
phase: "request",
|
|
92
|
+
type: "tool",
|
|
93
|
+
parents,
|
|
94
|
+
name: tool.name,
|
|
95
|
+
...(tool.name === "bash"
|
|
96
|
+
? { commandPreview: commandPreview(tool.arguments.command) }
|
|
97
|
+
: { summary: argumentSummary(tool.arguments) }),
|
|
98
|
+
}
|
|
99
|
+
: { phase: "request", type: "task", parents, name: task.kind };
|
|
100
|
+
notice.chatGuid = chatGuid;
|
|
101
|
+
notice.request = request;
|
|
102
|
+
const text = formatProgress(request);
|
|
103
|
+
const receipt = await deliverDirect(harness, { chatGuid, requestId: `progress:${task.id}`, text }, sender);
|
|
104
|
+
if (receipt.textStatus === "unknown")
|
|
105
|
+
console.warn("Progress delivery unknown", task.id, chatGuid);
|
|
106
|
+
}
|
|
107
|
+
async function settled(task, notice, elapsed) {
|
|
108
|
+
if (!notice.chatGuid || !notice.request || task.state.status !== "terminal")
|
|
109
|
+
return;
|
|
110
|
+
const settings = await readSettings(workingDir);
|
|
111
|
+
const chats = (await harness.snapshot(Chats, BACKGROUND_CONTEXT))?.items ?? [];
|
|
112
|
+
if (!isReplyEnabled(settings, notice.chatGuid) || !progressEnabled(settings, chats, notice.chatGuid))
|
|
113
|
+
return;
|
|
114
|
+
const outcome = task.state.outcome;
|
|
115
|
+
let success = outcome.status === "completed";
|
|
116
|
+
if (success && task.kind === "pi.tool" && outcome.status === "completed") {
|
|
117
|
+
const result = outcome.result;
|
|
118
|
+
const entryId = result && typeof result === "object" && "entryId" in result ? result.entryId : undefined;
|
|
119
|
+
const entry = typeof entryId === "number"
|
|
120
|
+
? await harness.commit((tx) => tx.entry(entryId), BACKGROUND_CONTEXT)
|
|
121
|
+
: undefined;
|
|
122
|
+
const message = entry?.model?.[0];
|
|
123
|
+
success = message?.role === "toolResult" && !message.isError;
|
|
124
|
+
}
|
|
125
|
+
const text = formatProgress({ ...notice.request, phase: "response", success, elapsedMs: elapsed });
|
|
126
|
+
const receipt = await deliverDirect(harness, {
|
|
127
|
+
chatGuid: notice.chatGuid,
|
|
128
|
+
requestId: `progress:${task.id}:done`,
|
|
129
|
+
text,
|
|
130
|
+
}, sender);
|
|
131
|
+
if (receipt.textStatus === "unknown")
|
|
132
|
+
console.warn("Progress completion delivery unknown", task.id, notice.chatGuid);
|
|
133
|
+
}
|
|
134
|
+
const unsubscribe = harness.subscribeCommits(({ changes }) => {
|
|
135
|
+
for (const change of changes) {
|
|
136
|
+
if (change.type !== "task" || change.value.kind === "pi.generation")
|
|
137
|
+
continue;
|
|
138
|
+
const task = change.value;
|
|
139
|
+
if (task.state.status === "terminal") {
|
|
140
|
+
seen.delete(task.id);
|
|
141
|
+
const notice = active.get(task.id);
|
|
142
|
+
active.delete(task.id);
|
|
143
|
+
if (notice) {
|
|
144
|
+
const elapsed = performance.now() - notice.admittedAt;
|
|
145
|
+
pending = pending
|
|
146
|
+
.then(() => settled(task, notice, elapsed))
|
|
147
|
+
.catch((error) => {
|
|
148
|
+
console.warn("Progress completion notification failed", task.id, error);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (task.state.status !== "pending" || seen.has(task.id))
|
|
154
|
+
continue;
|
|
155
|
+
seen.add(task.id);
|
|
156
|
+
const notice = { admittedAt: performance.now() };
|
|
157
|
+
active.set(task.id, notice);
|
|
158
|
+
// The synchronous observer must not call Session APIs or block adoption.
|
|
159
|
+
pending = pending
|
|
160
|
+
.then(() => notify(task, notice))
|
|
161
|
+
.catch((error) => {
|
|
162
|
+
console.warn("Progress notification failed", task.id, error);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
flush: () => pending,
|
|
168
|
+
async close() {
|
|
169
|
+
unsubscribe();
|
|
170
|
+
await pending;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DefaultPackageManager, formatSkillsForPrompt, loadProjectContextFiles, loadSkills, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineExtension, section } from "@earendil-works/pi-durable";
|
|
3
|
+
function preamble(workingDir) {
|
|
4
|
+
return `You are the user's best friend communicating via iMessage. Be concise. No emojis.
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
- Plain text only. Do not use Markdown formatting, double asterisks (**like this**), or [markdown](links).
|
|
8
|
+
- Reply in the same language the user is writing in.
|
|
9
|
+
- Name sources without URLs by default to avoid iMessage link previews. Include original URLs only when the user asks for links.
|
|
10
|
+
- Output ONLY the final message to the user. Never include planning, reasoning, analysis or meta-commentary in the reply.
|
|
11
|
+
|
|
12
|
+
## Environment
|
|
13
|
+
You are running directly on the host machine.
|
|
14
|
+
- Bash working directory: ${workingDir}
|
|
15
|
+
- Be careful with system modifications.
|
|
16
|
+
|
|
17
|
+
## Workspace Layout
|
|
18
|
+
${workingDir}/
|
|
19
|
+
├── settings.json # Bot configuration
|
|
20
|
+
├── extensions/<name>/ # Trusted business tools/tasks: index.ts and config.json
|
|
21
|
+
├── skills/ # Your reusable CLI tools; skills/file-memory is the memory store
|
|
22
|
+
├── attachments/<chat>/ # Files users shared, referenced by path in messages
|
|
23
|
+
└── durable/ # All conversations (JSONL); grep here to search old messages
|
|
24
|
+
|
|
25
|
+
## Messaging
|
|
26
|
+
Your final answer is sent to the chat automatically. Use the send_message tool for files or extra messages.
|
|
27
|
+
Six-hour compaction and workspace extension schedules run as native Durable background tasks. Their deadlines and recent execution records are visible on the read-only Scheduled web page.
|
|
28
|
+
You can create or update a trusted workspace extension in extensions/<name>/index.ts with its config.json, using the host-pinned Durable primitives. An extension can define multiple tasks and optional daily/interval schedules. Use reload_extensions (or the user command /reload) to apply code and configuration changes without interrupting current calls or resetting saved deadlines.
|
|
29
|
+
|
|
30
|
+
## Long-running work
|
|
31
|
+
- On a resumed or interrupted task, check its transcript and actual results first. An interrupted tool may already have acted: verify it instead of blindly repeating it.
|
|
32
|
+
|
|
33
|
+
## Skills (custom CLI tools)
|
|
34
|
+
Create reusable tools for on-demand tasks in ${workingDir}/skills/<name>/, each with a SKILL.md:
|
|
35
|
+
---
|
|
36
|
+
name: skill-name
|
|
37
|
+
description: What this skill does
|
|
38
|
+
---
|
|
39
|
+
Usage instructions and details here.`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The system prompt: product instructions, AGENTS files and skills, read once per load.
|
|
43
|
+
* `/reload` builds it again; the same extension name replaces the old one in place.
|
|
44
|
+
*/
|
|
45
|
+
export async function loadPrompt(workingDir, agentDir) {
|
|
46
|
+
const settings = SettingsManager.create(workingDir, agentDir, { projectTrusted: false });
|
|
47
|
+
const packages = new DefaultPackageManager({ cwd: workingDir, agentDir, settingsManager: settings });
|
|
48
|
+
// Discover installed user resources only; never install packages or execute SDK extensions headlessly.
|
|
49
|
+
const resources = await packages.resolve(async (source) => {
|
|
50
|
+
console.warn("Prompt resource unavailable; skipping missing package", source);
|
|
51
|
+
return "skip";
|
|
52
|
+
});
|
|
53
|
+
const agents = loadProjectContextFiles({ cwd: workingDir, agentDir });
|
|
54
|
+
const loadedSkills = loadSkills({
|
|
55
|
+
cwd: workingDir,
|
|
56
|
+
agentDir,
|
|
57
|
+
includeDefaults: false,
|
|
58
|
+
skillPaths: resources.skills
|
|
59
|
+
.filter((resource) => resource.enabled && resource.metadata.scope === "user")
|
|
60
|
+
.map((resource) => resource.path),
|
|
61
|
+
});
|
|
62
|
+
for (const diagnostic of loadedSkills.diagnostics)
|
|
63
|
+
console.warn("Skill resource diagnostic", diagnostic);
|
|
64
|
+
const skills = formatSkillsForPrompt(loadedSkills.skills, "read").trim();
|
|
65
|
+
const text = preamble(workingDir);
|
|
66
|
+
return defineExtension({
|
|
67
|
+
name: "prompt",
|
|
68
|
+
sections: [
|
|
69
|
+
section("preamble", () => text, { tag: false }),
|
|
70
|
+
section("project_context", () => agents.length === 0
|
|
71
|
+
? undefined
|
|
72
|
+
: [
|
|
73
|
+
"Project-specific instructions and guidelines:",
|
|
74
|
+
...agents.map(({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`),
|
|
75
|
+
].join("\n\n")),
|
|
76
|
+
section("skills", (input) => input.agent.tools.some((tool) => tool.name === "read") ? skills || undefined : undefined),
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineDoc } from "@earendil-works/pi-durable";
|
|
2
|
+
export const Deliveries = defineDoc({
|
|
3
|
+
kind: "imessage.deliveries",
|
|
4
|
+
version: 1,
|
|
5
|
+
scope: "conversation",
|
|
6
|
+
history: "latest",
|
|
7
|
+
fork: "initial",
|
|
8
|
+
initial: () => ({ answers: {} }),
|
|
9
|
+
checkpointWhen: () => false,
|
|
10
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { AssistantEntry, configure, defineDoc, defineExtension, defineTask, defineTool, GenerationTask, hook, InboxDoc, LiveDoc, section, } from "@earendil-works/pi-durable";
|
|
4
|
+
import { finalReplyText } from "../extensions/final-text.js";
|
|
5
|
+
import { Deliveries } from "./replies.js";
|
|
6
|
+
/** Every `/run`; ended runs stay listed so their last answers can still be delivered. */
|
|
7
|
+
export const Runs = defineDoc({
|
|
8
|
+
kind: "imessage.runs",
|
|
9
|
+
version: 1,
|
|
10
|
+
scope: "session",
|
|
11
|
+
initial: () => ({ items: [] }),
|
|
12
|
+
checkpointWhen: () => false,
|
|
13
|
+
});
|
|
14
|
+
/** Model-visible notes in the chat conversation about what happened in a run. */
|
|
15
|
+
export const RUN_RECORD = "imessage.run-record";
|
|
16
|
+
export function activeRun(runs, chat) {
|
|
17
|
+
return runs?.find((run) => run.chat === chat && run.status === "active");
|
|
18
|
+
}
|
|
19
|
+
export function runRecord(text, now) {
|
|
20
|
+
return { kind: RUN_RECORD, model: [{ role: "user", content: text, timestamp: now }] };
|
|
21
|
+
}
|
|
22
|
+
const instructions = [
|
|
23
|
+
"This conversation is a /run: the user asked you to keep working on the task without waiting for confirmation.",
|
|
24
|
+
"Every final answer is sent to the chat as it is written.",
|
|
25
|
+
"Call end_run when the task is done, when you need the user, or when there is no unfinished task; do not invent a new task.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
async function finish(tx, taskId, stopped, now) {
|
|
28
|
+
const run = (await tx.doc(Runs)).items.find((item) => item.taskId === taskId);
|
|
29
|
+
if (!run || run.status === "ended")
|
|
30
|
+
return;
|
|
31
|
+
if (stopped) {
|
|
32
|
+
// Withdraw answers that were not claimed for sending; a claimed send cannot be retracted.
|
|
33
|
+
const deliveries = await tx.doc(Deliveries, run.conversationId);
|
|
34
|
+
const entries = await tx.scanEntries({
|
|
35
|
+
conversationId: run.conversationId,
|
|
36
|
+
...(deliveries.scanned === undefined ? {} : { minEntryId: deliveries.scanned }),
|
|
37
|
+
}, 1000);
|
|
38
|
+
for (const entry of entries.items) {
|
|
39
|
+
const key = String(entry.id);
|
|
40
|
+
if (!AssistantEntry.is(entry) || !finalReplyText(entry.model?.[0]) || deliveries.answers[key])
|
|
41
|
+
continue;
|
|
42
|
+
if (deliveries.drafts?.[key] && deliveries.drafts[key].status !== "held")
|
|
43
|
+
continue;
|
|
44
|
+
deliveries.drafts ??= {};
|
|
45
|
+
deliveries.drafts[key] = { status: "cancelled" };
|
|
46
|
+
}
|
|
47
|
+
const drafts = deliveries.drafts ?? {};
|
|
48
|
+
for (const [key, draft] of Object.entries(drafts))
|
|
49
|
+
if (draft.status === "held")
|
|
50
|
+
drafts[key] = { status: "cancelled" };
|
|
51
|
+
run.endReason = "stopped";
|
|
52
|
+
}
|
|
53
|
+
run.endReason ??= now >= run.deadline ? "deadline" : "finished";
|
|
54
|
+
run.status = "ended";
|
|
55
|
+
// The chat conversation is idle while its run is active: ordinary messages go to the run.
|
|
56
|
+
await tx.appendEntry(run.chat, runRecord(`[/run ended: ${run.endReason}]`, now));
|
|
57
|
+
}
|
|
58
|
+
/** Owns the run conversation, so aborting it stops the run's work and withdraws its queued messages. */
|
|
59
|
+
export const RunTask = defineTask({
|
|
60
|
+
name: "imessage.run",
|
|
61
|
+
version: 1,
|
|
62
|
+
initial: () => ({ phase: "start" }),
|
|
63
|
+
phases: {
|
|
64
|
+
async start(task, runtime, context) {
|
|
65
|
+
const run = (await runtime.snapshot(Runs, context))?.items.find((item) => item.taskId === task.id);
|
|
66
|
+
const conversation = run && (await runtime.conversation(run.conversationId, context));
|
|
67
|
+
if (!conversation)
|
|
68
|
+
throw new Error("Run conversation is missing");
|
|
69
|
+
await conversation.submit({ type: "input", content: task.input.task, requestId: `run:${task.id}`, whenBusy: "steer" }, context);
|
|
70
|
+
await runtime.commit(() => ({ status: "running", checkpoint: { phase: "watch" } }), context);
|
|
71
|
+
},
|
|
72
|
+
async watch(task, runtime, context) {
|
|
73
|
+
const run = (await runtime.snapshot(Runs, context))?.items.find((item) => item.taskId === task.id);
|
|
74
|
+
const conversation = run && (await runtime.conversation(run.conversationId, context));
|
|
75
|
+
// onYield keeps the run conversation busy until end_run or the deadline; idle means the run is over.
|
|
76
|
+
await conversation?.waitForIdle(context);
|
|
77
|
+
await runtime.commit(async (tx) => {
|
|
78
|
+
await finish(tx, task.id, false, runtime.now());
|
|
79
|
+
return { status: "terminal", outcome: { status: "completed", result: null } };
|
|
80
|
+
}, context);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
async abort(task, runtime, context) {
|
|
84
|
+
await runtime.commit(async (tx) => {
|
|
85
|
+
await finish(tx, task.id, true, runtime.now());
|
|
86
|
+
return { status: "terminal", outcome: { status: "aborted" } };
|
|
87
|
+
}, context);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
/** Selected only by run conversations; its task definition resolves everywhere. */
|
|
91
|
+
export const RunExtension = defineExtension({
|
|
92
|
+
name: "run",
|
|
93
|
+
tasks: [RunTask],
|
|
94
|
+
sections: [section("run", () => instructions)],
|
|
95
|
+
tools: [
|
|
96
|
+
defineTool({
|
|
97
|
+
name: "end_run",
|
|
98
|
+
description: "End this /run when the task is done, you need the user, or nothing is left to do.",
|
|
99
|
+
parameters: Type.Object({ reason: Type.String() }),
|
|
100
|
+
replay: "safe",
|
|
101
|
+
async execute(args, api, context) {
|
|
102
|
+
await api.commit(async (tx) => {
|
|
103
|
+
const run = (await tx.doc(Runs)).items.find((item) => item.conversationId === api.conversationId && item.status === "active");
|
|
104
|
+
if (run)
|
|
105
|
+
run.endReason = args.reason;
|
|
106
|
+
}, context);
|
|
107
|
+
return { output: [{ type: "text", text: "The run ends after this answer." }] };
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
],
|
|
111
|
+
hooks: [
|
|
112
|
+
hook(GenerationTask, {
|
|
113
|
+
async onYield(_answer, api, context) {
|
|
114
|
+
const run = (await api.snapshot(Runs, context))?.items.find((item) => item.conversationId === api.conversationId);
|
|
115
|
+
if (run?.status !== "active" || run.endReason !== undefined)
|
|
116
|
+
return;
|
|
117
|
+
// The deadline only stops continuation; a turn in progress finishes.
|
|
118
|
+
const minutes = Math.floor((run.deadline - Date.now()) / 60000);
|
|
119
|
+
if (minutes < 1)
|
|
120
|
+
return;
|
|
121
|
+
return {
|
|
122
|
+
continue: `Continue the /run task. ${minutes} minutes left. Call end_run when it is done or you need the user.`,
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
/** Starts a run in a fork of the idle chat conversation, or moves the deadline of the active one. */
|
|
129
|
+
export function startRun(chat, chatGuid, deadline, task) {
|
|
130
|
+
return chat.commit(async (tx) => {
|
|
131
|
+
const newest = (await tx.scanEntries({ conversationId: chat.id }, 1)).items[0];
|
|
132
|
+
const runs = await tx.doc(Runs);
|
|
133
|
+
const active = activeRun(runs.items, chat.id);
|
|
134
|
+
if (active) {
|
|
135
|
+
active.deadline = deadline;
|
|
136
|
+
return { status: "updated", run: { ...active } };
|
|
137
|
+
}
|
|
138
|
+
const inbox = await tx.doc(InboxDoc, chat.id);
|
|
139
|
+
if ((await tx.doc(LiveDoc, chat.id)).run || inbox.items.length > 0)
|
|
140
|
+
return { status: "busy" };
|
|
141
|
+
const taskId = await tx.createTask(RunTask, { task }, { ownership: { kind: "conversation" } });
|
|
142
|
+
const ownership = { ownership: { kind: "task", taskId } };
|
|
143
|
+
// A fork inherits the chat's context without copying or summarizing it.
|
|
144
|
+
const conversation = newest
|
|
145
|
+
? await tx.forkConversation(chat.id, newest.id, ownership)
|
|
146
|
+
: await tx.createConversation(ownership);
|
|
147
|
+
await configure(tx, conversation.id, { extensions: { add: [RunExtension] } });
|
|
148
|
+
const run = {
|
|
149
|
+
chatGuid,
|
|
150
|
+
chat: chat.id,
|
|
151
|
+
conversationId: conversation.id,
|
|
152
|
+
taskId,
|
|
153
|
+
deadline,
|
|
154
|
+
status: "active",
|
|
155
|
+
};
|
|
156
|
+
runs.items.push(run);
|
|
157
|
+
await tx.appendEntry(chat.id, runRecord(`[/run started: ${task}]`, Date.now()));
|
|
158
|
+
return { status: "started", run: { ...run } };
|
|
159
|
+
}, BACKGROUND_CONTEXT);
|
|
160
|
+
}
|