my-durable 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/ai_docs/durable_pi/announcement.md +860 -0
- package/ai_docs/durable_pi/changelog.md +51 -0
- package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
- package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
- package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
- package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
- package/ai_docs/durable_pi/readme.md +617 -0
- package/ai_docs/durable_pi/sources.md +22 -0
- package/ai_docs/durable_pi/spec.md +4696 -0
- package/ai_docs/durable_pi/vacation-planner.md +57 -0
- package/ai_docs/pi/chord.md +268 -0
- package/ai_docs/pi/cli-integration.md +110 -0
- package/ai_docs/pi/cli.md +325 -0
- package/ai_docs/pi/codemode.md +211 -0
- package/ai_docs/pi/compaction.md +467 -0
- package/ai_docs/pi/configuration.md +51 -0
- package/ai_docs/pi/containerization.md +187 -0
- package/ai_docs/pi/custom-provider.md +177 -0
- package/ai_docs/pi/environment-variables.md +102 -0
- package/ai_docs/pi/extensions.md +278 -0
- package/ai_docs/pi/how-pi-works.md +53 -0
- package/ai_docs/pi/index.md +43 -0
- package/ai_docs/pi/json.md +230 -0
- package/ai_docs/pi/keybindings.md +198 -0
- package/ai_docs/pi/llama-cpp.md +118 -0
- package/ai_docs/pi/mcp.md +274 -0
- package/ai_docs/pi/message-types.md +265 -0
- package/ai_docs/pi/models.md +209 -0
- package/ai_docs/pi/overview.md +166 -0
- package/ai_docs/pi/packages.md +133 -0
- package/ai_docs/pi/pi-agent-core.md +568 -0
- package/ai_docs/pi/pi-ai.md +1942 -0
- package/ai_docs/pi/prompt-templates.md +63 -0
- package/ai_docs/pi/providers.md +204 -0
- package/ai_docs/pi/quickstart.md +140 -0
- package/ai_docs/pi/readme.md +99 -0
- package/ai_docs/pi/rpc-commands.md +864 -0
- package/ai_docs/pi/rpc-extension-ui.md +204 -0
- package/ai_docs/pi/rpc.md +197 -0
- package/ai_docs/pi/sdk.md +151 -0
- package/ai_docs/pi/security.md +103 -0
- package/ai_docs/pi/session-format.md +297 -0
- package/ai_docs/pi/sessions.md +70 -0
- package/ai_docs/pi/settings.md +173 -0
- package/ai_docs/pi/shell-aliases.md +97 -0
- package/ai_docs/pi/skills.md +97 -0
- package/ai_docs/pi/slash-commands.md +64 -0
- package/ai_docs/pi/sources.md +57 -0
- package/ai_docs/pi/terminal-setup.md +223 -0
- package/ai_docs/pi/termux.md +121 -0
- package/ai_docs/pi/themes.md +140 -0
- package/ai_docs/pi/tmux.md +59 -0
- package/ai_docs/pi/toolkit.md +168 -0
- package/ai_docs/pi/tui.md +129 -0
- package/ai_docs/pi/usage.md +98 -0
- package/ai_docs/pi/virtual-models.md +118 -0
- package/ai_docs/pi/windows.md +71 -0
- package/apps/dpi-backend/.nvmrc +1 -0
- package/apps/dpi-backend/API.md +99 -0
- package/apps/dpi-backend/README.md +119 -0
- package/apps/dpi-backend/deploy/backend.env.example +9 -0
- package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
- package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
- package/apps/dpi-backend/package.json +33 -0
- package/apps/dpi-backend/prompts/one_sentence.md +3 -0
- package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
- package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
- package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
- package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
- package/apps/dpi-backend/scripts/token.ts +57 -0
- package/apps/dpi-backend/src/addons.ts +139 -0
- package/apps/dpi-backend/src/auth.ts +20 -0
- package/apps/dpi-backend/src/config.ts +54 -0
- package/apps/dpi-backend/src/contract-check.ts +118 -0
- package/apps/dpi-backend/src/errors.ts +50 -0
- package/apps/dpi-backend/src/extensions.ts +510 -0
- package/apps/dpi-backend/src/faux.ts +80 -0
- package/apps/dpi-backend/src/history.ts +165 -0
- package/apps/dpi-backend/src/idempotency.ts +20 -0
- package/apps/dpi-backend/src/jev.ts +43 -0
- package/apps/dpi-backend/src/jobs.ts +790 -0
- package/apps/dpi-backend/src/main.ts +76 -0
- package/apps/dpi-backend/src/model-catalog.ts +180 -0
- package/apps/dpi-backend/src/pairing-files.ts +142 -0
- package/apps/dpi-backend/src/pairing.ts +459 -0
- package/apps/dpi-backend/src/routes.ts +217 -0
- package/apps/dpi-backend/src/shared-state.ts +85 -0
- package/apps/dpi-backend/src/store.ts +1240 -0
- package/apps/dpi-backend/src/streams.ts +256 -0
- package/apps/dpi-backend/src/tunnel.ts +955 -0
- package/apps/dpi-backend/tsconfig.json +13 -0
- package/apps/dpi-playground/README.md +83 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
- package/apps/dpi-playground/package.json +37 -0
- package/apps/dpi-sdk/README.md +74 -0
- package/apps/dpi-sdk/package.json +20 -0
- package/apps/dpi-sdk/scripts/net.ts +73 -0
- package/apps/dpi-sdk/src/api.ts +631 -0
- package/apps/dpi-sdk/src/app-state.ts +111 -0
- package/apps/dpi-sdk/src/client.ts +330 -0
- package/apps/dpi-sdk/src/errors.ts +16 -0
- package/apps/dpi-sdk/src/jobs.ts +99 -0
- package/apps/dpi-sdk/src/models.ts +472 -0
- package/apps/dpi-sdk/src/node/chunk.ts +488 -0
- package/apps/dpi-sdk/src/node/config.ts +175 -0
- package/apps/dpi-sdk/src/node/drivers.ts +402 -0
- package/apps/dpi-sdk/src/node/ops.ts +114 -0
- package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
- package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
- package/apps/dpi-sdk/src/node/registry.ts +390 -0
- package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
- package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
- package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
- package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
- package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
- package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
- package/apps/dpi-sdk/src/sse.ts +117 -0
- package/apps/dpi-sdk/src/transcript.ts +291 -0
- package/apps/dpi-sdk/tsconfig.json +13 -0
- package/apps/dpi-sdk/w +20 -0
- package/apps/dpi-tui/README.md +54 -0
- package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
- package/apps/dpi-tui/package.json +20 -0
- package/apps/dpi-tui/src/app.ts +433 -0
- package/apps/dpi-tui/src/commands.ts +151 -0
- package/apps/dpi-tui/src/main.ts +122 -0
- package/apps/dpi-tui/src/pairing.ts +287 -0
- package/apps/dpi-tui/src/pickers.ts +238 -0
- package/apps/dpi-tui/src/render.ts +164 -0
- package/apps/dpi-tui/src/theme.ts +57 -0
- package/apps/dpi-tui/tsconfig.json +14 -0
- package/bin/device.mjs +5 -0
- package/bin/hub.mjs +5 -0
- package/bin/tui.mjs +5 -0
- package/deploy/Caddyfile +120 -0
- package/docs/verification.md +102 -0
- package/dpi-network.example.yaml +62 -0
- package/images/01_durable_agent_network.svg +185 -0
- package/images/02_pi_made_durable.svg +204 -0
- package/images/03_restart_mid_tool.svg +168 -0
- package/images/07_one_api_every_client.svg +123 -0
- package/images/08_device_session_agent.svg +123 -0
- package/images/09_compact_keep_history.svg +143 -0
- package/images/10_shared_app_state.svg +177 -0
- package/images/11_durable_jobs_wait.svg +141 -0
- package/images/12_tools_change_mid_run.svg +231 -0
- package/images/13_hook_blocks_tool.svg +122 -0
- package/images/14_fork_at_any_entry.svg +119 -0
- package/images/15_web_client.png +0 -0
- package/images/16_terminal_client.svg +43 -0
- package/images/17_devices_are_data.svg +182 -0
- package/images/18_should_every_agent_be_durable.svg +118 -0
- package/images/19_sqlite_on_every_device.svg +155 -0
- package/images/20_admission_and_effects.svg +134 -0
- package/justfile +33 -0
- package/package.json +41 -4
- package/scripts/verify.mjs +210 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import type { SharedState } from "./shared-state.js";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { readState, type AppState } from "../../dpi-sdk/src/app-state.js";
|
|
5
|
+
import type { SessionSummary, ThinkingLevel } from "../../dpi-sdk/src/api.js";
|
|
6
|
+
import { THINKING_LEVELS } from "../../dpi-sdk/src/api.js";
|
|
7
|
+
import type { WireEntry } from "../../dpi-sdk/src/api.js";
|
|
8
|
+
import { jobRunDone, type JobRun } from "../../dpi-sdk/src/jobs.js";
|
|
9
|
+
import {
|
|
10
|
+
defineDoc,
|
|
11
|
+
defineTool,
|
|
12
|
+
defineExtension,
|
|
13
|
+
section,
|
|
14
|
+
configure,
|
|
15
|
+
AssistantEntry,
|
|
16
|
+
type EntryRecord,
|
|
17
|
+
type ToolExecutionApi,
|
|
18
|
+
} from "@earendil-works/pi-durable";
|
|
19
|
+
import type { Context } from "@earendil-works/chord";
|
|
20
|
+
import { JobRuns, jobDef, resolveJobInput, type Jobs } from "./jobs.js";
|
|
21
|
+
import { searchHistory } from "./history.js";
|
|
22
|
+
|
|
23
|
+
export const Workspace = defineDoc<AppState>({
|
|
24
|
+
kind: "demo.workspace",
|
|
25
|
+
version: 1,
|
|
26
|
+
scope: "conversation",
|
|
27
|
+
history: "rewindable",
|
|
28
|
+
fork: "asOf",
|
|
29
|
+
initial: () => ({ goal: "Explore durable agents", counter: 0, notes: [] }),
|
|
30
|
+
});
|
|
31
|
+
const Receipt = defineDoc<{ applied: boolean; result: string }>({
|
|
32
|
+
kind: "demo.receipt",
|
|
33
|
+
version: 1,
|
|
34
|
+
scope: "task",
|
|
35
|
+
initial: () => ({ applied: false, result: "" }),
|
|
36
|
+
});
|
|
37
|
+
/** ping_on_call_engineer pages once per agent; this records the page. */
|
|
38
|
+
const Pager = defineDoc<{ paged: boolean; at: number; page: string; summary: string }>({
|
|
39
|
+
kind: "demo.pager",
|
|
40
|
+
version: 1,
|
|
41
|
+
scope: "conversation",
|
|
42
|
+
history: "latest",
|
|
43
|
+
fork: "initial",
|
|
44
|
+
initial: () => ({ paged: false, at: 0, page: "", summary: "" }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const pause = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
48
|
+
|
|
49
|
+
/** One line per phase change, for the run_durable_job call's live output. */
|
|
50
|
+
function progressLines(run: JobRun) {
|
|
51
|
+
return run.phases.flatMap((phase, i) => {
|
|
52
|
+
const n = `${i + 1}/${run.phases.length} ${phase.label}`;
|
|
53
|
+
if (phase.status === "done") return [`✓ ${n}: ${phase.summary ?? "done"}`];
|
|
54
|
+
if (phase.status === "running") return [`▸ ${n}`];
|
|
55
|
+
if (phase.status === "waiting") return [`⏸ ${n}: waiting for a person to approve in the chat`];
|
|
56
|
+
if (phase.status === "failed" || phase.status === "aborted") return [`✗ ${n}: ${phase.summary ?? phase.status}`];
|
|
57
|
+
return [];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const phaseLabel = (run: JobRun) => run.phases.find((p) => p.id === run.phase)?.label;
|
|
62
|
+
const clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
63
|
+
|
|
64
|
+
/** One run as check_durable_job reports it: compact enough for a model, complete enough to act on. */
|
|
65
|
+
function runReport(run: JobRun, now: number) {
|
|
66
|
+
return {
|
|
67
|
+
taskId: run.id,
|
|
68
|
+
job: run.job,
|
|
69
|
+
title: run.title,
|
|
70
|
+
run: run.run,
|
|
71
|
+
status: run.status,
|
|
72
|
+
...(jobRunDone(run) ? {} : { phase: phaseLabel(run) }),
|
|
73
|
+
...(run.approval === "pending" ? { next: "Waiting for a person to approve in the chat" } : {}),
|
|
74
|
+
...(run.approval && run.approval !== "pending" ? { approval: run.approval } : {}),
|
|
75
|
+
startedBy: run.startedBy,
|
|
76
|
+
background: run.background,
|
|
77
|
+
seconds: Math.round(((run.endedAt ?? now) - run.startedAt) / 100) / 10,
|
|
78
|
+
...(run.resumed ? { resumedAfterRestart: run.resumed } : {}),
|
|
79
|
+
...(run.result ? { result: run.result } : {}),
|
|
80
|
+
...(run.error ? { error: run.error } : {}),
|
|
81
|
+
phases: run.phases.map((p) => ({
|
|
82
|
+
phase: p.label,
|
|
83
|
+
status: p.status,
|
|
84
|
+
...(p.summary ? { summary: p.summary } : {}),
|
|
85
|
+
...(p.progress ? { progress: `${p.progress.done}/${p.progress.total}` } : {}),
|
|
86
|
+
...(p.steps.length
|
|
87
|
+
? { steps: p.steps.map((s) => `${s.status} ${s.kind} ${s.label}${s.detail ? `: ${clip(s.detail.replace(/\s+/g, " "), 160)}` : ""}`) }
|
|
88
|
+
: {}),
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Creates a session on this device, exactly once per `key`, and optionally sends its first prompt. */
|
|
94
|
+
export type SpawnSession = (input: {
|
|
95
|
+
key: string;
|
|
96
|
+
name: string;
|
|
97
|
+
cwd: string;
|
|
98
|
+
model: { provider: string; modelId: string };
|
|
99
|
+
thinkingLevel?: ThinkingLevel;
|
|
100
|
+
prompt?: string;
|
|
101
|
+
}) => Promise<{ session: SessionSummary; submissionId?: number }>;
|
|
102
|
+
|
|
103
|
+
export function createPlayground(shared: SharedState, sessionId: string, spawn: SpawnSession, jobs: Jobs) {
|
|
104
|
+
const readAppState = defineTool({
|
|
105
|
+
name: "read_state",
|
|
106
|
+
description:
|
|
107
|
+
"Read the shared App State object used by every session, conversation and fork, or a field at a JSON Pointer path. Empty path reads the whole object.",
|
|
108
|
+
parameters: Type.Object({ path: Type.Optional(Type.String()) }),
|
|
109
|
+
replay: "safe",
|
|
110
|
+
execute: async (args) => {
|
|
111
|
+
const { value } = await shared.snapshot();
|
|
112
|
+
return {
|
|
113
|
+
output: [{ type: "text", text: JSON.stringify(readState(value, args.path ?? "")) }],
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const updateState = defineTool({
|
|
118
|
+
name: "update_state",
|
|
119
|
+
description:
|
|
120
|
+
'CRUD the same shared App State for every agent and fork in this demo. create adds a new field, update replaces an existing value, delete removes it, increment adds a signed number. value_json must be JSON-encoded text: 3 for a number, true for a boolean, "hello" (including quotes) for a string, {"enabled":true} for an object, or [1,2] for an array. Omit value_json for delete. Use JSON Pointer paths: /counter, /settings/enabled, /notes/- to append. Escape ~ as ~0 and / as ~1. Empty path with update replaces the whole root object, with delete clears it. Read first with read_state. UI controls update live. Changes are atomic and replay-safe.',
|
|
121
|
+
parameters: Type.Object({
|
|
122
|
+
operation: Type.Union([
|
|
123
|
+
Type.Literal("create"),
|
|
124
|
+
Type.Literal("update"),
|
|
125
|
+
Type.Literal("delete"),
|
|
126
|
+
Type.Literal("increment"),
|
|
127
|
+
]),
|
|
128
|
+
path: Type.String(),
|
|
129
|
+
value_json: Type.Optional(
|
|
130
|
+
Type.String({
|
|
131
|
+
description:
|
|
132
|
+
"JSON-encoded value. Objects and arrays must be encoded as JSON here, not passed as native tool argument objects.",
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
136
|
+
replay: "safe",
|
|
137
|
+
execute: async (args, api, ctx) => {
|
|
138
|
+
const next = await shared.change(
|
|
139
|
+
{
|
|
140
|
+
operation: args.operation,
|
|
141
|
+
path: args.path,
|
|
142
|
+
value: args.value_json === undefined ? undefined : JSON.parse(args.value_json),
|
|
143
|
+
},
|
|
144
|
+
`agent:${sessionId}:${api.taskId}`,
|
|
145
|
+
);
|
|
146
|
+
const value = await api.commit(async (tx) => {
|
|
147
|
+
const receipt = await tx.doc(Receipt, api.taskId);
|
|
148
|
+
if (!receipt.applied) {
|
|
149
|
+
await tx.appendEntry(api.conversationId, {
|
|
150
|
+
kind: "demo.state-change",
|
|
151
|
+
data: { taskId: api.taskId, text: `App State: ${args.operation} ${args.path || "/"}` },
|
|
152
|
+
});
|
|
153
|
+
receipt.applied = true;
|
|
154
|
+
receipt.result = JSON.stringify(next);
|
|
155
|
+
}
|
|
156
|
+
return receipt.result;
|
|
157
|
+
}, ctx);
|
|
158
|
+
return { output: [{ type: "text", text: value }] };
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const recentRuns = async (api: Pick<ToolExecutionApi, "conversationId" | "snapshot">, ctx: Context) =>
|
|
162
|
+
Object.values((await api.snapshot(JobRuns, api.conversationId, ctx))?.items ?? {}).sort((a, b) => b.id - a.id);
|
|
163
|
+
const listJobs = defineTool({
|
|
164
|
+
name: "list_durable_jobs",
|
|
165
|
+
description:
|
|
166
|
+
"List the durable jobs you can start with run_durable_job: each job's id, what it does, its named phases, the inputs it takes (type, default, limits), whether it waits for a person's approval, the App State path where every run saves its result, and its recent runs in this conversation with their task ids for check_durable_job.",
|
|
167
|
+
parameters: Type.Object({}),
|
|
168
|
+
replay: "safe",
|
|
169
|
+
execute: async (_args, api, ctx) => {
|
|
170
|
+
const runs = await recentRuns(api, ctx);
|
|
171
|
+
return {
|
|
172
|
+
output: [
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: JSON.stringify(
|
|
176
|
+
jobs.defs.map((d) => ({
|
|
177
|
+
job: d.id,
|
|
178
|
+
title: d.title,
|
|
179
|
+
summary: d.summary,
|
|
180
|
+
phases: d.phases.map((p) => `${p.label}: ${p.summary}`),
|
|
181
|
+
inputs: Object.fromEntries(
|
|
182
|
+
d.inputs.map((i) => [
|
|
183
|
+
i.name,
|
|
184
|
+
{ type: i.type, default: i.default, ...(i.min !== undefined ? { min: i.min, max: i.max } : {}), description: i.description },
|
|
185
|
+
]),
|
|
186
|
+
),
|
|
187
|
+
needsApproval: d.approval,
|
|
188
|
+
savesTo: d.resultPath,
|
|
189
|
+
typicalSeconds: d.seconds,
|
|
190
|
+
recentRuns: runs
|
|
191
|
+
.filter((r) => r.job === d.id)
|
|
192
|
+
.slice(0, 5)
|
|
193
|
+
.map((r) => ({ taskId: r.id, run: r.run, status: r.status, phase: phaseLabel(r) })),
|
|
194
|
+
})),
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const runJob = defineTool({
|
|
202
|
+
name: "run_durable_job",
|
|
203
|
+
description:
|
|
204
|
+
'Start a durable job: a repeatable workflow with named phases that runs as one checkpointed task and shows live in the chat. Call list_durable_jobs first for ids and inputs. It runs in the background: you get its task id at once, the conversation owns the run, and you check it with check_durable_job. Set wait true only when the user wants you to wait for the result; then the call owns the run, an interrupt stops it, and you get the outcome of every phase. Example: {"job":"workspace-snapshot","input":{"path":"."}}.',
|
|
205
|
+
parameters: Type.Object({
|
|
206
|
+
job: Type.String({ description: "Job id from list_durable_jobs." }),
|
|
207
|
+
input: Type.Optional(
|
|
208
|
+
Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]), {
|
|
209
|
+
description: "The job's inputs by name. Omitted inputs use their defaults.",
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
wait: Type.Optional(
|
|
213
|
+
Type.Boolean({ description: "Wait for the run to finish, including any approval. Default false." }),
|
|
214
|
+
),
|
|
215
|
+
}),
|
|
216
|
+
replay: "safe",
|
|
217
|
+
execute: async (args, api, ctx) => {
|
|
218
|
+
const def = jobDef(args.job);
|
|
219
|
+
if (!def) throw Error(`Unknown job ${args.job}. Jobs: ${jobs.defs.map((d) => d.id).join(", ")}`);
|
|
220
|
+
const input = resolveJobInput(def, args.input ?? {});
|
|
221
|
+
const wait = args.wait === true;
|
|
222
|
+
// A replay after a crash finds the run this call started instead of starting another.
|
|
223
|
+
const taskId = await api.commit(async (tx) => {
|
|
224
|
+
const receipt = await tx.doc(Receipt, api.taskId);
|
|
225
|
+
if (receipt.applied) return Number(receipt.result);
|
|
226
|
+
const id = await jobs.start(tx, api.conversationId, {
|
|
227
|
+
def,
|
|
228
|
+
input,
|
|
229
|
+
startedBy: "agent",
|
|
230
|
+
toolCallId: api.callId,
|
|
231
|
+
...(wait ? { owner: api.taskId } : {}),
|
|
232
|
+
now: Date.now(),
|
|
233
|
+
});
|
|
234
|
+
receipt.applied = true;
|
|
235
|
+
receipt.result = String(id);
|
|
236
|
+
return id as number;
|
|
237
|
+
}, ctx);
|
|
238
|
+
await api.details({ taskId, job: def.id }, ctx);
|
|
239
|
+
const read = async () => (await api.snapshot(JobRuns, api.conversationId, ctx))?.items[String(taskId)];
|
|
240
|
+
if (!wait) {
|
|
241
|
+
const run = await read();
|
|
242
|
+
return {
|
|
243
|
+
output: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `Started ${def.title} run #${run?.run ?? "?"} as durable task #${taskId}. It runs in the background, shows live in the chat and saves its result to App State ${def.resultPath}. Check it with check_durable_job {"task_id":${taskId}}.`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
details: { taskId, job: def.id },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const printed = new Set<string>();
|
|
253
|
+
let run = await read();
|
|
254
|
+
for (;;) {
|
|
255
|
+
for (const line of run ? progressLines(run) : [])
|
|
256
|
+
if (!printed.has(line)) {
|
|
257
|
+
printed.add(line);
|
|
258
|
+
api.output(`${line}\n`);
|
|
259
|
+
}
|
|
260
|
+
if (!run || jobRunDone(run)) break;
|
|
261
|
+
await pause(400);
|
|
262
|
+
run = await read();
|
|
263
|
+
}
|
|
264
|
+
const ok = run?.status === "completed";
|
|
265
|
+
return {
|
|
266
|
+
output: [
|
|
267
|
+
{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: run
|
|
270
|
+
? `${def.title} run #${run.run} ${run.status}${ok ? `: ${run.result}` : run.error ? `: ${run.error}` : ""}.\n${progressLines(run).join("\n")}${ok ? `\nResult saved to App State ${def.resultPath}.` : ""}`
|
|
271
|
+
: `${def.title} task #${taskId} has no record.`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
isError: !ok,
|
|
275
|
+
details: { taskId, job: def.id },
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const checkJob = defineTool({
|
|
280
|
+
name: "check_durable_job",
|
|
281
|
+
description:
|
|
282
|
+
"Check durable job runs in this conversation. With task_id, one run in full: status, current phase, every phase's outcome and steps (commands, the subagent's tool calls, App State writes), approval, result or error. With job, that job's latest run. With neither, a short list of recent runs. Task ids come from run_durable_job and list_durable_jobs.",
|
|
283
|
+
parameters: Type.Object({
|
|
284
|
+
task_id: Type.Optional(Type.Integer({ minimum: 1, description: "The run's task id." })),
|
|
285
|
+
job: Type.Optional(Type.String({ description: "A job id: check its latest run." })),
|
|
286
|
+
}),
|
|
287
|
+
replay: "safe",
|
|
288
|
+
execute: async (args, api, ctx) => {
|
|
289
|
+
const runs = await recentRuns(api, ctx);
|
|
290
|
+
const run =
|
|
291
|
+
args.task_id !== undefined
|
|
292
|
+
? runs.find((r) => r.id === args.task_id)
|
|
293
|
+
: args.job !== undefined
|
|
294
|
+
? runs.find((r) => r.job === args.job)
|
|
295
|
+
: undefined;
|
|
296
|
+
if (args.task_id === undefined && args.job === undefined)
|
|
297
|
+
return {
|
|
298
|
+
output: [
|
|
299
|
+
{
|
|
300
|
+
type: "text",
|
|
301
|
+
text: JSON.stringify(
|
|
302
|
+
runs.slice(0, 10).map((r) => ({ taskId: r.id, job: r.job, run: r.run, status: r.status, phase: phaseLabel(r) })),
|
|
303
|
+
),
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
if (!run)
|
|
308
|
+
throw Error(
|
|
309
|
+
`No run ${args.task_id !== undefined ? `with task id ${args.task_id}` : `of ${args.job}`} in this conversation. Runs: ${runs.map((r) => `${r.id} (${r.job} #${r.run})`).join(", ") || "none"}`,
|
|
310
|
+
);
|
|
311
|
+
return { output: [{ type: "text", text: JSON.stringify(runReport(run, Date.now())) }] };
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const searchHistoryTool = defineTool({
|
|
315
|
+
name: "search_history",
|
|
316
|
+
description:
|
|
317
|
+
"Search your own full stored history, including entries from before a reset, handoff or compaction that you can no longer see. Filter by words (query), by tool name (tool), by entry kind, or by entry number range (from_entry, to_entry). Pass entry to read one entry in full by its number. Results come newest first (order oldest to reverse) and are paged with limit and offset; use next_offset for the next page. Each result says whether the entry is active, compacted or before reset.",
|
|
318
|
+
parameters: Type.Object({
|
|
319
|
+
query: Type.Optional(Type.String({ description: "Words that must all appear, case-insensitive." })),
|
|
320
|
+
tool: Type.Optional(Type.String({ description: "Only calls to and results from this tool, for example bash." })),
|
|
321
|
+
entry: Type.Optional(Type.Integer({ minimum: 1, description: "Read this one entry in full." })),
|
|
322
|
+
kind: Type.Optional(
|
|
323
|
+
Type.Union([
|
|
324
|
+
Type.Literal("user"),
|
|
325
|
+
Type.Literal("assistant"),
|
|
326
|
+
Type.Literal("tool_call"),
|
|
327
|
+
Type.Literal("tool_result"),
|
|
328
|
+
Type.Literal("event"),
|
|
329
|
+
]),
|
|
330
|
+
),
|
|
331
|
+
from_entry: Type.Optional(Type.Integer({ minimum: 1, description: "Oldest entry number to include." })),
|
|
332
|
+
to_entry: Type.Optional(Type.Integer({ minimum: 1, description: "Newest entry number to include." })),
|
|
333
|
+
order: Type.Optional(Type.Union([Type.Literal("newest"), Type.Literal("oldest")])),
|
|
334
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50, description: "Results per page. Default 10." })),
|
|
335
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Results to skip. Default 0." })),
|
|
336
|
+
}),
|
|
337
|
+
replay: "safe",
|
|
338
|
+
execute: async (args, api, ctx) => {
|
|
339
|
+
const { entries, activeFrom } = await api.commit(async (tx) => {
|
|
340
|
+
const all: EntryRecord[] = [];
|
|
341
|
+
let page = await tx.scanEntries({ conversationId: api.conversationId }, 500);
|
|
342
|
+
all.push(...page.items);
|
|
343
|
+
while (page.next && all.length < 10_000) {
|
|
344
|
+
page = await tx.scanEntries({ conversationId: api.conversationId }, 500, page.next);
|
|
345
|
+
all.push(...page.items);
|
|
346
|
+
}
|
|
347
|
+
return { entries: all, activeFrom: (await tx.latestHeadMarker(api.conversationId))?.head as number | undefined };
|
|
348
|
+
}, ctx);
|
|
349
|
+
const resets = entries.filter((e) => e.kind === "pi.reset").map((e) => e.id as number);
|
|
350
|
+
const result = searchHistory(entries as unknown as WireEntry[], args, {
|
|
351
|
+
activeFrom,
|
|
352
|
+
lastReset: resets.length ? Math.max(...resets) : undefined,
|
|
353
|
+
});
|
|
354
|
+
return { output: [{ type: "text", text: JSON.stringify(result) }] };
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
// No replay policy: the default `unsafe` means a crash mid-call returns an interrupted result and never pages twice.
|
|
358
|
+
const pingOnCall = defineTool({
|
|
359
|
+
name: "ping_on_call_engineer",
|
|
360
|
+
description:
|
|
361
|
+
"Page the human on-call engineer with a short summary. A real side effect: it is not replay-safe, so a crash returns an interrupted result instead of paging again, and it works once per agent. Only use it when the user asks you to page someone.",
|
|
362
|
+
parameters: Type.Object({
|
|
363
|
+
summary: Type.String({ description: "What the engineer needs to know, one or two sentences." }),
|
|
364
|
+
severity: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("high"), Type.Literal("critical")])),
|
|
365
|
+
}),
|
|
366
|
+
execute: async (args, api, ctx) => {
|
|
367
|
+
const prior = await api.snapshot(Pager, api.conversationId, ctx);
|
|
368
|
+
if (prior?.paged)
|
|
369
|
+
throw Error(
|
|
370
|
+
`Already paged at ${new Date(prior.at).toISOString()} (${prior.page}). ping_on_call_engineer works once per agent.`,
|
|
371
|
+
);
|
|
372
|
+
const severity = args.severity ?? "high";
|
|
373
|
+
const page = `PG-${String(api.taskId).padStart(4, "0")}`;
|
|
374
|
+
api.output("Dialing the on-call engineer…\n");
|
|
375
|
+
await pause(1500);
|
|
376
|
+
await api.commit(async (tx) => {
|
|
377
|
+
const pager = await tx.doc(Pager, api.conversationId);
|
|
378
|
+
if (pager.paged) throw Error(`Already paged (${pager.page}).`);
|
|
379
|
+
Object.assign(pager, { paged: true, at: Date.now(), page, summary: args.summary.slice(0, 300) });
|
|
380
|
+
}, ctx);
|
|
381
|
+
api.output(`Paged on-call, ${severity} severity: ${page}\nWaiting for acknowledgement…\n`);
|
|
382
|
+
await pause(1500);
|
|
383
|
+
api.output("Acknowledged.\n");
|
|
384
|
+
return {
|
|
385
|
+
output: [
|
|
386
|
+
{
|
|
387
|
+
type: "text",
|
|
388
|
+
text: `Paged the on-call engineer (${page}, ${severity}). They acknowledged. Do not page again for this.`,
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
const subagent = defineTool({
|
|
395
|
+
name: "subagent",
|
|
396
|
+
description:
|
|
397
|
+
"Delegate a self-contained task to an owned child conversation. Parent interruption also interrupts the child.",
|
|
398
|
+
parameters: Type.Object({ task: Type.String() }),
|
|
399
|
+
replay: "safe",
|
|
400
|
+
execute: async (args, api, ctx) => {
|
|
401
|
+
const id = await api.commit(async (tx) => {
|
|
402
|
+
const found = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
403
|
+
if (found) return found.id;
|
|
404
|
+
const child = await tx.createConversation({
|
|
405
|
+
ownership: { kind: "task", taskId: api.taskId },
|
|
406
|
+
});
|
|
407
|
+
await configure(tx, child.id, { tools: { remove: [subagent] } });
|
|
408
|
+
return child.id;
|
|
409
|
+
}, ctx);
|
|
410
|
+
await api.details({ conversationId: id }, ctx);
|
|
411
|
+
const child = await api.conversation(id, ctx);
|
|
412
|
+
if (!child) throw Error("Child missing");
|
|
413
|
+
const result = await (
|
|
414
|
+
await child.submit(
|
|
415
|
+
{ type: "input", content: args.task, requestId: `child:${api.taskId}` },
|
|
416
|
+
ctx,
|
|
417
|
+
)
|
|
418
|
+
).wait(ctx);
|
|
419
|
+
const answer =
|
|
420
|
+
result.status === "done" && result.type === "input"
|
|
421
|
+
? await api.commit((tx) => tx.entry(AssistantEntry, result.answer), ctx)
|
|
422
|
+
: undefined;
|
|
423
|
+
return {
|
|
424
|
+
output: [
|
|
425
|
+
{ type: "text", text: answer ? JSON.stringify(answer.model) : JSON.stringify(result) },
|
|
426
|
+
],
|
|
427
|
+
details: { conversationId: id },
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
const startSession = defineTool({
|
|
432
|
+
name: "start_session",
|
|
433
|
+
description:
|
|
434
|
+
"Start a new session on this device: its own harness, database and working directory, independent of this one. It keeps running if you are interrupted. Returns immediately; you do not wait for it.",
|
|
435
|
+
parameters: Type.Object({
|
|
436
|
+
name: Type.String({ description: "Session name shown in the network tree." }),
|
|
437
|
+
cwd: Type.String({
|
|
438
|
+
description: "Existing folder on this device. Relative paths resolve against your working directory.",
|
|
439
|
+
}),
|
|
440
|
+
model: Type.Optional(
|
|
441
|
+
Type.String({ description: "Model id from this device's model list. Default: your current model." }),
|
|
442
|
+
),
|
|
443
|
+
prompt: Type.Optional(
|
|
444
|
+
Type.String({ description: "First message for the new session. Default: none, the session starts idle." }),
|
|
445
|
+
),
|
|
446
|
+
}),
|
|
447
|
+
replay: "safe",
|
|
448
|
+
execute: async (args, api, ctx) => {
|
|
449
|
+
const parent = await api.agent(ctx);
|
|
450
|
+
if (!parent.model) throw Error("This agent has no model to copy");
|
|
451
|
+
const cwd = args.cwd.startsWith("~") || isAbsolute(args.cwd) ? args.cwd : resolve(parent.cwd ?? ".", args.cwd);
|
|
452
|
+
const thinking = THINKING_LEVELS.find((level) => level === parent.thinkingLevel);
|
|
453
|
+
const { session, submissionId } = await spawn({
|
|
454
|
+
key: `start_session:${sessionId}:${api.taskId}`,
|
|
455
|
+
name: args.name,
|
|
456
|
+
cwd,
|
|
457
|
+
model: { provider: parent.model.provider, modelId: args.model ?? parent.model.modelId },
|
|
458
|
+
thinkingLevel: thinking,
|
|
459
|
+
prompt: args.prompt?.trim() || undefined,
|
|
460
|
+
});
|
|
461
|
+
const sent =
|
|
462
|
+
submissionId === undefined
|
|
463
|
+
? "It has no prompt yet."
|
|
464
|
+
: `Sent its first message as submission #${submissionId}. It runs on its own.`;
|
|
465
|
+
return {
|
|
466
|
+
output: [
|
|
467
|
+
{
|
|
468
|
+
type: "text",
|
|
469
|
+
text: `Started session "${session.name}" (${session.id}) in ${session.cwd} with ${session.model}. ${sent}`,
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
details: { sessionId: session.id, ...(submissionId === undefined ? {} : { submissionId }) },
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
const handoff = defineTool({
|
|
477
|
+
name: "handoff",
|
|
478
|
+
description:
|
|
479
|
+
"Start a fresh model context with a concise handoff note. Old transcript stays stored.",
|
|
480
|
+
parameters: Type.Object({ note: Type.String() }),
|
|
481
|
+
execute: async (args) => ({
|
|
482
|
+
output: [{ type: "text", text: "Context handed off." }],
|
|
483
|
+
control: { handoff: args.note },
|
|
484
|
+
}),
|
|
485
|
+
});
|
|
486
|
+
return defineExtension({
|
|
487
|
+
name: "playground",
|
|
488
|
+
tasks: jobs.tasks,
|
|
489
|
+
tools: [
|
|
490
|
+
readAppState,
|
|
491
|
+
updateState,
|
|
492
|
+
listJobs,
|
|
493
|
+
runJob,
|
|
494
|
+
checkJob,
|
|
495
|
+
searchHistoryTool,
|
|
496
|
+
pingOnCall,
|
|
497
|
+
subagent,
|
|
498
|
+
startSession,
|
|
499
|
+
handoff,
|
|
500
|
+
],
|
|
501
|
+
sections: [
|
|
502
|
+
section(
|
|
503
|
+
"playground",
|
|
504
|
+
() =>
|
|
505
|
+
`You are a concise coding agent in the Durable Pi playground. Explain only what matters. Use only the tools you are offered; read only agents get no tools that write. Use read_state to inspect App State and update_state to create, update, delete, or increment any field. App State is shared across all agents, sessions, conversations and forks in this demo. It survives browser closure, interrupted agents, and backend restarts. Forks use the current shared object rather than a historical copy. Read before writing because other agents and humans can change it. Use only read_state and update_state for this object, never inspect or edit its database files. App State is a flexible JSON object: numbers, booleans, strings, arrays, nested objects, and null all render as live controls in the UI. Read before editing. Use list_durable_jobs and run_durable_job to run a repeatable durable job (a named workflow with checkpointed phases) when asked; pass only the inputs list_durable_jobs documents. Runs continue in the background: check them with check_durable_job, and wait only when asked. Use search_history to look back at earlier messages, tool calls or entries by number, including what came before a reset, handoff or compaction. Use ping_on_call_engineer only when asked to page someone; it works once. Use subagent only when explicitly asked to delegate. Use start_session only when asked to start a new session; it runs on its own. Never claim an action happened without using its tool. Background runs finish independently; do not wait in bash. A browser disconnect does not stop your work.`,
|
|
506
|
+
{ tag: false },
|
|
507
|
+
),
|
|
508
|
+
],
|
|
509
|
+
});
|
|
510
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// A deterministic, free model for tests and offline demos (DPI_PROVIDER=faux).
|
|
2
|
+
// "!<command>" → one bash tool call running <command>, then a reply quoting its output
|
|
3
|
+
// "slow: <text>" → streams <text> slowly
|
|
4
|
+
// "spawn: <name>[ | <prompt>]" → one start_session call in the current folder
|
|
5
|
+
// "jobs?" → one list_durable_jobs call
|
|
6
|
+
// "job: <id>[ <input json>][ wait]" → one run_durable_job call (wait: the call waits for the run)
|
|
7
|
+
// "check:[ <task id>|<job id>]" → one check_durable_job call
|
|
8
|
+
// "search: <words>" → one search_history call
|
|
9
|
+
// "page: <summary>" → one ping_on_call_engineer call
|
|
10
|
+
// "Release check: …" (the release-check job's subagent) → one bash call
|
|
11
|
+
// anything else → "Echo: <text>"
|
|
12
|
+
import type { MutableModels } from "@earendil-works/pi-ai/models";
|
|
13
|
+
import {
|
|
14
|
+
fauxAssistantMessage,
|
|
15
|
+
fauxProvider,
|
|
16
|
+
fauxText,
|
|
17
|
+
fauxToolCall,
|
|
18
|
+
type FauxResponseFactory,
|
|
19
|
+
} from "@earendil-works/pi-ai/providers/faux";
|
|
20
|
+
|
|
21
|
+
export const fauxModels = [{ id: "echo", name: "Faux Echo" }] as const;
|
|
22
|
+
|
|
23
|
+
type AnyMessage = { role: string; content?: unknown; toolCallId?: string };
|
|
24
|
+
const textOf = (m?: AnyMessage) =>
|
|
25
|
+
typeof m?.content === "string"
|
|
26
|
+
? m.content
|
|
27
|
+
: Array.isArray(m?.content)
|
|
28
|
+
? (m!.content as { type: string; text?: string }[]).map((b) => (b.type === "text" ? (b.text ?? "") : "")).join("")
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
export function installFaux(models: MutableModels) {
|
|
32
|
+
const handle = fauxProvider({
|
|
33
|
+
provider: "faux",
|
|
34
|
+
models: fauxModels.map((m) => ({ id: m.id, name: m.name, contextWindow: 128000, maxTokens: 8192 })),
|
|
35
|
+
tokensPerSecond: 400,
|
|
36
|
+
});
|
|
37
|
+
const reply: FauxResponseFactory = (context) => {
|
|
38
|
+
handle.appendResponses([reply]);
|
|
39
|
+
const messages = context.messages as unknown as AnyMessage[];
|
|
40
|
+
const last = messages.at(-1);
|
|
41
|
+
if (last?.role === "toolResult")
|
|
42
|
+
return fauxAssistantMessage(fauxText(`Tool finished: ${textOf(last).trim().slice(0, 400)}`));
|
|
43
|
+
const prompt = textOf([...messages].reverse().find((m) => m.role === "user")).trim();
|
|
44
|
+
if (prompt.startsWith("!"))
|
|
45
|
+
return fauxAssistantMessage([fauxText("Running it."), fauxToolCall("bash", { command: prompt.slice(1).trim() })], {
|
|
46
|
+
stopReason: "toolUse",
|
|
47
|
+
});
|
|
48
|
+
if (prompt.startsWith("spawn:")) {
|
|
49
|
+
const [name, first] = prompt.slice(6).split("|").map((s) => s.trim());
|
|
50
|
+
return fauxAssistantMessage(
|
|
51
|
+
[fauxText("Starting it."), fauxToolCall("start_session", { name, cwd: ".", ...(first ? { prompt: first } : {}) })],
|
|
52
|
+
{ stopReason: "toolUse" },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const call = (name: string, args: Parameters<typeof fauxToolCall>[1]) =>
|
|
56
|
+
fauxAssistantMessage([fauxText(`Calling ${name}.`), fauxToolCall(name, args)], { stopReason: "toolUse" });
|
|
57
|
+
if (prompt === "jobs?") return call("list_durable_jobs", {});
|
|
58
|
+
if (prompt.startsWith("job:")) {
|
|
59
|
+
const m = prompt.slice(4).trim().match(/^(\S+)\s*(\{.*\})?\s*(wait)?$/);
|
|
60
|
+
return call("run_durable_job", {
|
|
61
|
+
job: m?.[1] ?? "",
|
|
62
|
+
...(m?.[2] ? { input: JSON.parse(m[2]) } : {}),
|
|
63
|
+
...(m?.[3] ? { wait: true } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (prompt.startsWith("check:")) {
|
|
67
|
+
const ref = prompt.slice(6).trim();
|
|
68
|
+
return call("check_durable_job", /^\d+$/.test(ref) ? { task_id: Number(ref) } : ref ? { job: ref } : {});
|
|
69
|
+
}
|
|
70
|
+
if (prompt.startsWith("search:")) return call("search_history", { query: prompt.slice(7).trim() });
|
|
71
|
+
if (prompt.startsWith("page:")) return call("ping_on_call_engineer", { summary: prompt.slice(5).trim() });
|
|
72
|
+
if (prompt.startsWith("Release check:"))
|
|
73
|
+
return call("bash", { command: "git log --oneline -5 2>/dev/null || echo 'no git history'; ls | head -5" });
|
|
74
|
+
if (prompt.startsWith("slow:")) return fauxAssistantMessage(fauxText(prompt.slice(5).trim().repeat(4)));
|
|
75
|
+
return fauxAssistantMessage(fauxText(`Echo: ${prompt}`));
|
|
76
|
+
};
|
|
77
|
+
handle.setResponses([reply]);
|
|
78
|
+
models.setProvider(handle.provider);
|
|
79
|
+
return handle;
|
|
80
|
+
}
|