pi-harness-runtime 0.10.17 → 0.10.19

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.
Files changed (40) hide show
  1. package/package.json +3 -4
  2. package/packages/auth/src/run-minimax-auth.ts +0 -0
  3. package/prompts/tencentdb-connect.md +35 -0
  4. package/prompts/tencentdb-setup.md +101 -0
  5. package/prompts/tencentdb.md +88 -0
  6. package/.versionrc.js +0 -32
  7. package/cli.js +0 -112
  8. package/footer-status.js +0 -155
  9. package/harness/agent-handoff.js +0 -123
  10. package/harness/auto-compact.js +0 -243
  11. package/harness/auto-quota-resume.js +0 -104
  12. package/harness/blackboard.js +0 -258
  13. package/harness/context-compact-orchestrator.js +0 -365
  14. package/harness/context-window-manager.js +0 -330
  15. package/harness/continue-prompt.js +0 -164
  16. package/harness/e2e/minimax-quota-parser.js +0 -52
  17. package/harness/e2e/minimax-quota-scraper.js +0 -475
  18. package/harness/e2e/openai-quota-scraper.js +0 -332
  19. package/harness/e2e/playwright-runner.js +0 -165
  20. package/harness/e2e/quota-status.js +0 -140
  21. package/harness/e2e/test-engine.js +0 -290
  22. package/harness/forked-summarizer.js +0 -212
  23. package/harness/index.js +0 -54
  24. package/harness/job-state-machine.js +0 -277
  25. package/harness/loop-runtime.js +0 -531
  26. package/harness/master-planner.js +0 -229
  27. package/harness/notification-events.js +0 -233
  28. package/harness/output-limit-handler.js +0 -233
  29. package/harness/partial-recovery.js +0 -413
  30. package/harness/project-detector/detector.js +0 -283
  31. package/harness/repair-engine.js +0 -256
  32. package/harness/session-memory.js +0 -293
  33. package/harness/task-graph.js +0 -88
  34. package/index.js +0 -1121
  35. package/mirror.js +0 -205
  36. package/proactive-compact.js +0 -42
  37. package/renderer.js +0 -134
  38. package/status-parsers.js +0 -63
  39. package/tracker.js +0 -49
  40. package/windows.js +0 -90
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "0.10.17",
3
+ "version": "0.10.19",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,16 +16,15 @@
16
16
  "auth:minimax:scrape": "bun packages/auth/src/run-minimax-auth.ts scrape",
17
17
  "skills:sync": "bun scripts/skills-sync.ts",
18
18
  "skills:sync:check": "bun scripts/skills-sync.ts --check-only",
19
- "build": "for pkg in packages/*/; do [ -f \"${pkg}tsconfig.json\" ] && [ \"$pkg\" != \"packages/provider-router/\" ] && node_modules/.bin/tsc -p \"${pkg}tsconfig.json\" --skipLibCheck > /dev/null 2>&1 || true; done && node_modules/.bin/tsc -p tsconfig.harness.json --skipLibCheck > /dev/null 2>&1"
19
+ "build": "for pkg in packages/*/; do [ -f \"${pkg}tsconfig.json\" ] && [ \"$pkg\" != \"packages/provider-router/\" ] && node_modules/.bin/tsc -p \"${pkg}tsconfig.json\" --skipLibCheck > /dev/null 2>&1 || true; done",
20
+ "prepublishOnly": "bun run build"
20
21
  },
21
22
  "bin": {
22
23
  "harness-auth": "packages/auth/src/run-minimax-auth.ts"
23
24
  },
24
25
  "files": [
25
26
  "*.ts",
26
- "*.js",
27
27
  "harness/**/*.ts",
28
- "harness/**/*.js",
29
28
  "packages/**/src/**/*.{ts,js}",
30
29
  "packages/**/dist/**/*.{js,d.ts,js.map,d.ts.map}",
31
30
  "skills/**/*",
File without changes
@@ -0,0 +1,35 @@
1
+ ---
2
+ description: Connect to your own TencentDB-Agent-Memory server
3
+ argument-hint: "<action>"
4
+ ---
5
+ # TencentDB-Agent-Memory Connection Setup
6
+
7
+ ## Available Actions
8
+
9
+ Use `/tencentdb-connect` with one of these actions:
10
+
11
+ | Action | Description |
12
+ |--------|-------------|
13
+ | `connect` | Connect to an existing server |
14
+ | `deploy` | Deploy a new server |
15
+ | `test` | Test current connection |
16
+ | `status` | Check server health |
17
+
18
+ ## Connect to Existing Server
19
+
20
+ Run `/tencentdb-connect connect` and provide:
21
+
22
+ 1. **Server URL** - Your TencentDB server address
23
+ - Example: `https://memory.your-domain.com`
24
+
25
+ 2. **Service ID** - Usually `default`
26
+
27
+ 3. **User Key** - Provided by server admin
28
+
29
+ ## Deploy New Server
30
+
31
+ Run `/tencentdb-connect deploy` for guided VPS deployment.
32
+
33
+ ## Test Connection
34
+
35
+ Run `/tencentdb-connect test` to verify your setup.
@@ -0,0 +1,101 @@
1
+ ---
2
+ description: Interactively configure TencentDB-Agent-Memory connection
3
+ argument-hint: "<server-url> [service-id] [user-key]"
4
+ ---
5
+ # Setup TencentDB-Agent-Memory Connection
6
+
7
+ ${1:---}
8
+
9
+ ## Configuration Options
10
+
11
+ **Arguments:**
12
+ - `$1` = Server URL (e.g., `https://memory.example.com`)
13
+ - `$2` = Service ID (default: `default`)
14
+ - `$3` = User Key (from server admin)
15
+
16
+ ## Step-by-Step Setup
17
+
18
+ ### Step 1: Enter Server URL
19
+
20
+ Please provide your TencentDB-Agent-Memory server URL:
21
+
22
+ ```
23
+ https://your-server.com
24
+ ```
25
+
26
+ ### Step 2: Enter Service ID
27
+
28
+ Enter your service ID (or press Enter for `default`):
29
+
30
+ ```
31
+ default
32
+ ```
33
+
34
+ ### Step 3: Enter User Key
35
+
36
+ Enter your user key:
37
+
38
+ ```
39
+ your-user-key-here
40
+ ```
41
+
42
+ ## Configuration Methods
43
+
44
+ ### Option A: Environment File (.env)
45
+
46
+ Create or update `.env` in your project:
47
+
48
+ ```bash
49
+ TENANTDB_URL=${1:-https://your-server.com}
50
+ TENANTDB_SERVICE_ID=${2:-default}
51
+ TENANTDB_USER_KEY=${3}
52
+ ```
53
+
54
+ ### Option B: Settings JSON
55
+
56
+ Add to `.pi/settings.json`:
57
+
58
+ ```json
59
+ {
60
+ "tencentdb": {
61
+ "url": "${1:-https://your-server.com}",
62
+ "serviceId": "${2:-default}",
63
+ "userKey": "${3}"
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Verification
69
+
70
+ Test your connection:
71
+
72
+ ```bash
73
+ curl -X GET "${1:-https://your-server.com}/health" \
74
+ -H "x-tdai-service-id: ${2:-default}" \
75
+ -H "x-tdai-user-key: ${3}"
76
+ ```
77
+
78
+ Expected response:
79
+ ```json
80
+ {"status": "ok", "version": "1.0.0"}
81
+ ```
82
+
83
+ ## Usage Examples
84
+
85
+ ### Search Knowledge
86
+ ```bash
87
+ curl -X POST "${1}/api/v1/memory/search" \
88
+ -H "x-tdai-service-id: ${2}" \
89
+ -H "x-tdai-user-key: ${3}" \
90
+ -H "Content-Type: application/json" \
91
+ -d '{"query": "your search query"}'
92
+ ```
93
+
94
+ ### Sync Skills
95
+ ```bash
96
+ curl -X POST "${1}/api/v1/skills/sync" \
97
+ -H "x-tdai-service-id: ${2}" \
98
+ -H "x-tdai-user-key: ${3}" \
99
+ -H "Content-Type: application/json" \
100
+ -d '{"skills": [...]}'
101
+ ```
@@ -0,0 +1,88 @@
1
+ ---
2
+ description: Configure TencentDB-Agent-Memory connection
3
+ argument-hint: "[server-url]"
4
+ ---
5
+ # Configure TencentDB-Agent-Memory
6
+
7
+ This command helps you connect to your own TencentDB-Agent-Memory server for centralized knowledge retrieval.
8
+
9
+ ## Options
10
+
11
+ ### 1. Use Default Server (Recommended for Testing)
12
+ If you don't have your own server, you can use the public demo:
13
+ - **URL**: `https://https://your-memory-server.example.com`
14
+ - **Note**: This is a shared server, data may be visible to others
15
+
16
+ ### 2. Deploy Your Own Server
17
+ Deploy TencentDB-Agent-Memory on your own VPS:
18
+
19
+ **Minimal Requirements:**
20
+ - 2 vCPU, 4GB RAM, 50GB SSD
21
+ - Ubuntu 22.04 LTS
22
+ - Docker installed
23
+
24
+ **Quick Deploy:**
25
+ ```bash
26
+ # SSH into your VPS
27
+ ssh user@your-vps-ip
28
+
29
+ # Install Docker if needed
30
+ curl -fsSL https://get.docker.com | sh
31
+
32
+ # Deploy the server
33
+ docker run -d \
34
+ --name tencentdb-memory \
35
+ -p 8080:8080 \
36
+ -e OPENAI_API_KEY=your-api-key \
37
+ ghcr.io/tencentcloud/tdai-memory:latest
38
+ ```
39
+
40
+ ### 3. Connect via MCP
41
+ After deployment, configure the MCP connection:
42
+
43
+ **Server URL**: `${1:-https://your-vps-ip:8080}`
44
+
45
+ **Required Headers**:
46
+ ```
47
+ x-tdai-service-id: default
48
+ x-tdai-user-key: YOUR_USER_KEY
49
+ ```
50
+
51
+ **Available Endpoints**:
52
+ - `POST /api/v1/memory/search` - Search knowledge base
53
+ - `POST /api/v1/memory/conversation/search` - Search conversations
54
+ - `GET /api/v1/health` - Health check
55
+
56
+ ## Configuration
57
+
58
+ ### Quick Setup
59
+
60
+ To connect to a TencentDB-Agent-Memory server, provide:
61
+
62
+ 1. **Server URL** (e.g., `https://memory.example.com`)
63
+ 2. **Service ID** (usually `default`)
64
+ 3. **User Key** (from your server admin)
65
+
66
+ ### Environment Variables
67
+
68
+ Add to your `.env` or shell profile:
69
+
70
+ ```bash
71
+ # TencentDB-Agent-Memory Configuration
72
+ TENANTDB_URL=https://your-server.com
73
+ TENANTDB_SERVICE_ID=default
74
+ TENANTDB_USER_KEY=your-user-key
75
+ ```
76
+
77
+ ## Next Steps
78
+
79
+ After configuration, you can:
80
+
81
+ 1. **Search Knowledge**: Use `/search <query>` to find skills and documentation
82
+ 2. **Sync Skills**: Upload your local skills to the centralized server
83
+ 3. **Query Code**: Search across all indexed codebases
84
+
85
+ ## Need Help?
86
+
87
+ - **Documentation**: https://github.com/TencentCloud/TencentDB-Agent-Memory
88
+ - **Issues**: Report bugs on the GitHub repository
package/.versionrc.js DELETED
@@ -1,32 +0,0 @@
1
- // standard-version config
2
- // https://github.com/conventional-changelog/standard-version
3
- //
4
- // ESM version (project uses "type": "module")
5
-
6
- export default {
7
- types: [
8
- { type: "feat", section: "Features" },
9
- { type: "fix", section: "Bug Fixes" },
10
- { type: "perf", section: "Performance" },
11
- { type: "refactor", section: "Refactoring" },
12
- { type: "docs", section: "Documentation" },
13
- { type: "test", section: "Tests" },
14
- { type: "ci", section: "CI/CD" },
15
- { type: "chore", section: "Maintenance", hidden: false },
16
- ],
17
- bumpFiles: [
18
- {
19
- filename: "package.json",
20
- type: "json",
21
- },
22
- ],
23
- packageFiles: ["package.json"],
24
- bumpInChangelog: "package.json",
25
- tagPrefix: "v",
26
- commitUrlFormat:
27
- "https://github.com/ManotLuijiu/pi-harness-runtime/commit/{{hash}}",
28
- compareUrlFormat:
29
- "https://github.com/ManotLuijiu/pi-harness-runtime/compare/v{{previousTag}}...v{{currentTag}}",
30
- issueUrlFormat:
31
- "https://github.com/ManotLuijiu/pi-harness-runtime/issues/{{id}}",
32
- };
package/cli.js DELETED
@@ -1,112 +0,0 @@
1
- /**
2
- * Pure helper functions — testable without pi context.
3
- * Imported by tracker.ts, mirror.ts, windows.ts, renderer.ts.
4
- */
5
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
- import { dirname, join } from "node:path";
7
- import { homedir } from "node:os";
8
- /** Resolve the ~/.pi/usage-status/ directory. Override with PI_USAGE_DIR env var (testing). */
9
- export function getUsageDir() {
10
- const override = process.env.PI_USAGE_DIR;
11
- if (override)
12
- return override;
13
- return join(homedir(), ".pi", "usage-status");
14
- }
15
- /** Resolve the JSONL usage log path. */
16
- export function getUsageLogPath() {
17
- return join(getUsageDir(), "usage.jsonl");
18
- }
19
- /** Resolve the mirror JSON path. */
20
- export function getMirrorPath() {
21
- return join(getUsageDir(), "mirror.json");
22
- }
23
- /** Ensure the directory exists. Idempotent. */
24
- export function ensureUsageDir() {
25
- const dir = getUsageDir();
26
- if (!existsSync(dir)) {
27
- mkdirSync(dir, { recursive: true });
28
- }
29
- }
30
- /** Read the JSONL usage log as an array of UsageRecord. Returns [] if missing. */
31
- export function readJsonl(path) {
32
- if (!existsSync(path))
33
- return [];
34
- const text = readFileSync(path, "utf-8");
35
- const out = [];
36
- for (const line of text.split("\n")) {
37
- const trimmed = line.trim();
38
- if (!trimmed)
39
- continue;
40
- try {
41
- out.push(JSON.parse(trimmed));
42
- }
43
- catch {
44
- // skip corrupted line
45
- }
46
- }
47
- return out;
48
- }
49
- /** Append a single JSON line to a JSONL file. */
50
- export function appendJsonl(path, record) {
51
- ensureUsageDir();
52
- const line = JSON.stringify(record) + "\n";
53
- appendFileSync(path, line, "utf-8");
54
- }
55
- /** Read JSON file safely. Returns null if missing or corrupted. */
56
- export function readJson(path) {
57
- if (!existsSync(path))
58
- return null;
59
- try {
60
- return JSON.parse(readFileSync(path, "utf-8"));
61
- }
62
- catch {
63
- return null;
64
- }
65
- }
66
- /** Write JSON file (creates parent dir if needed). */
67
- export function writeJson(path, data) {
68
- ensureUsageDir();
69
- if (!existsSync(dirname(path))) {
70
- mkdirSync(dirname(path), { recursive: true });
71
- }
72
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
73
- }
74
- /** Format milliseconds as "Xh Ym" or "Xd Yh" or "Xm" or "Xs". */
75
- export function formatDuration(ms) {
76
- const abs = Math.max(0, Math.floor(ms / 1000));
77
- const sec = abs % 60;
78
- const min = Math.floor(abs / 60) % 60;
79
- const hr = Math.floor(abs / 3600) % 24;
80
- const day = Math.floor(abs / 86400);
81
- if (day > 0)
82
- return `${day}d ${hr}h`;
83
- if (hr > 0)
84
- return `${hr}h ${min}m`;
85
- if (min > 0)
86
- return `${min}m`;
87
- return `${sec}s`;
88
- }
89
- /** Format a timestamp as relative time "X min ago". */
90
- export function formatRelative(fromIso, nowMs) {
91
- const fromMs = Date.parse(fromIso);
92
- if (isNaN(fromMs))
93
- return "unknown";
94
- const deltaMs = nowMs - fromMs;
95
- return formatDuration(deltaMs) + " ago";
96
- }
97
- /** Format a token count with k/M suffix. */
98
- export function formatTokens(n) {
99
- if (n < 1000)
100
- return String(n);
101
- if (n < 1_000_000)
102
- return `${(n / 1000).toFixed(1)}k`;
103
- return `${(n / 1_000_000).toFixed(2)}M`;
104
- }
105
- /** Format USD cost with 2-4 decimals depending on size. */
106
- export function formatUsd(n) {
107
- if (n < 0.01)
108
- return `$${n.toFixed(4)}`;
109
- if (n < 100)
110
- return `$${n.toFixed(2)}`;
111
- return `$${n.toFixed(0)}`;
112
- }
package/footer-status.js DELETED
@@ -1,155 +0,0 @@
1
- import { parseContextWindowStatusLine, parseQuotaUsageStatusLine, } from "./status-parsers.ts";
2
- import { getProviderLabel } from "./packages/types/src/ai-providers.js";
3
- import { providerHasContinuousScrape, providerHasTUISignal, } from "./packages/providers/src/provider-id.js";
4
- export function parseFooterStatusValue(value) {
5
- const quota = parseQuotaUsageStatusLine(value);
6
- if (quota) {
7
- return { kind: "quota", value: quota };
8
- }
9
- const contextWindow = parseContextWindowStatusLine(value);
10
- if (contextWindow) {
11
- return { kind: "context-window", value: contextWindow };
12
- }
13
- const todayMatch = value.match(/^today:\s*([\d,.]+)k tok\s*·\s*\$([\d,.]+)/i);
14
- if (todayMatch) {
15
- const tokens = Number.parseFloat(todayMatch[1].replace(/,/g, "")) * 1000;
16
- const cost = Number.parseFloat(todayMatch[2].replace(/,/g, ""));
17
- if (Number.isFinite(tokens) && Number.isFinite(cost)) {
18
- return {
19
- kind: "today",
20
- value: { tokens, cost },
21
- };
22
- }
23
- }
24
- return { kind: "unknown", value: null };
25
- }
26
- /**
27
- * Get display label for a provider id.
28
- * Uses the canonical getProviderLabel() from packages/types/src/ai-providers.ts
29
- * which handles all 9 known providers: minimax, openai, anthropic, glm,
30
- * openrouter, openai-codex, deepseek, gemini, kimi.
31
- */
32
- function providerDisplayName(provider) {
33
- if (!provider)
34
- return "Provider";
35
- return getProviderLabel(provider);
36
- }
37
- /**
38
- * Hint shown when the provider has no usage data yet.
39
- * Covers all 9 known providers from KNOWN_AI_PROVIDERS.
40
- */
41
- function missingDataHint(provider, hasCookieSource) {
42
- if (!provider)
43
- return "no usage source configured";
44
- // MiniMax: needs cookies for continuous scrape
45
- if (provider === "minimax") {
46
- return hasCookieSource
47
- ? "no data yet (updates after first scrape)"
48
- : "drop minimax cookies into ~/.pi-harness-runtime/cookies/";
49
- }
50
- // TUI signal providers: one-shot signal on limit hit
51
- if (providerHasTUISignal(provider)) {
52
- return "no signal yet (updates on first limit hit)";
53
- }
54
- // Providers without any tracking implementation yet
55
- // deepseek, gemini, kimi, or any unknown provider
56
- if (provider === "deepseek") {
57
- return "deepseek usage tracking not yet implemented";
58
- }
59
- if (provider === "gemini") {
60
- return "gemini usage tracking not yet implemented";
61
- }
62
- if (provider === "kimi") {
63
- return "kimi usage tracking not yet implemented";
64
- }
65
- // Generic fallback for any other unknown provider
66
- if (!hasCookieSource)
67
- return "no usage source configured";
68
- return "no data yet";
69
- }
70
- /** Normalize an unknown mirror record into a ProviderMirrorRecord view. */
71
- function toProviderView(mirror) {
72
- if (!mirror)
73
- return null;
74
- const m = mirror;
75
- if (typeof m.provider === "string")
76
- return m;
77
- // Legacy flat-shape: already a single record without a provider wrapper.
78
- return {
79
- synced_at: m.synced_at ?? new Date().toISOString(),
80
- provider: m.provider ?? "",
81
- source: "scrape",
82
- model: m.model,
83
- h5_used_pct: m.h5_used_pct,
84
- h5_resets_at: m.h5_resets_at,
85
- weekly_used_pct: m.weekly_used_pct,
86
- weekly_resets_at: m.weekly_resets_at,
87
- };
88
- }
89
- export function buildFooterStatusValue(local, mirror, freshness, hasCookieSource = true, activeProvider = null) {
90
- const view = toProviderView(mirror);
91
- const provider = activeProvider ?? view?.provider ?? null;
92
- const label = providerDisplayName(provider);
93
- // TUI-signal exhaustion path: fires when limit is hit (one-shot signal).
94
- // This takes priority over continuous data to show the most recent state.
95
- if (provider &&
96
- view &&
97
- view.exhausted &&
98
- (view.limitType !== undefined ||
99
- view.resets_at !== undefined ||
100
- view.h5_resets_at !== undefined)) {
101
- const reset = view.resets_at ?? view.h5_resets_at ?? "soon";
102
- const limitType = view.limitType ?? "tokens";
103
- return `${label}: limit hit (${limitType}), reset ${reset}`;
104
- }
105
- // Continuous data path: providers with continuous scrape (MiniMax has 5h+weekly, OpenAI has weekly-only)
106
- if (provider &&
107
- providerHasContinuousScrape(provider) &&
108
- view &&
109
- freshness !== "expired" &&
110
- (view.h5_used_pct !== undefined || view.weekly_used_pct !== undefined)) {
111
- const weeklyPct = view.weekly_used_pct ?? 0;
112
- const weeklyLeft = Math.max(0, 100 - weeklyPct);
113
- const weeklyResets = view.weekly_resets_at ?? "soon";
114
- let statusLine;
115
- if (view.h5_used_pct !== undefined) {
116
- // MiniMax: has both 5h and weekly windows
117
- const h5Pct = view.h5_used_pct;
118
- const h5Left = Math.max(0, 100 - h5Pct);
119
- statusLine = `5h: ${h5Left.toFixed(0)}% left · week: ${weeklyLeft.toFixed(0)}% left`;
120
- }
121
- else {
122
- // OpenAI: weekly-only (no 5h window)
123
- statusLine = `week: ${weeklyLeft.toFixed(0)}% left (resets ${weeklyResets})`;
124
- }
125
- const freshnessSuffix = freshness === "fresh" || freshness === "missing" ? "" : ` · ${freshness}`;
126
- return `${label}: ${statusLine}${freshnessSuffix}`;
127
- }
128
- // TUI signal providers (OpenAI, Anthropic, GLM, OpenRouter):
129
- // Show monitoring status when we have a record but haven't hit limits.
130
- // These providers only emit data when a limit is hit.
131
- if (provider &&
132
- providerHasTUISignal(provider) &&
133
- view &&
134
- freshness !== "expired") {
135
- // If exhausted is not set, we're monitoring normally
136
- if (!view.exhausted) {
137
- return `${label}: monitoring (no limits hit)`;
138
- }
139
- }
140
- // Discoverable hint when we have no data yet.
141
- if (provider &&
142
- (!view || freshness === "expired" || freshness === "missing")) {
143
- const hint = missingDataHint(provider, hasCookieSource);
144
- // MiniMax-on-fresh-machine still gets the cookie hint; for others
145
- // the hint explains the signal-driven design.
146
- if (provider === "minimax" && !hasCookieSource) {
147
- return `${label}: 5h: -- (${hint})`;
148
- }
149
- return `${label}: 5h: -- · week: -- (${hint})`;
150
- }
151
- // Silent fallback: only reached if we genuinely don't know the provider.
152
- // No "5h/week: --" line here because we'd be making up data.
153
- const todayStr = `${(local.today.tokens / 1000).toFixed(1)}k tok · $${local.today.cost.toFixed(3)}`;
154
- return `today: ${todayStr}`;
155
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * Agent Handoff Protocol — RFC-0012
3
- *
4
- * Clean handoff between agents with context transfer.
5
- * Ensures continuity when switching agents mid-task.
6
- */
7
- import { writeJson, readJson } from "../cli.ts";
8
- // @ts-expect-error - Bun has built-in Node.js types
9
- import { join } from "node:path";
10
- export class AgentHandoffProtocol {
11
- rootDir;
12
- constructor(rootDir) {
13
- this.rootDir = rootDir;
14
- }
15
- /**
16
- * Create a handoff context for switching agents
17
- */
18
- createHandoff(jobId, taskId, fromAgent, toAgent, currentState) {
19
- const events = this.loadHandoffHistory(jobId, taskId);
20
- return {
21
- jobId,
22
- taskId,
23
- fromAgent,
24
- toAgent,
25
- sharedFiles: [],
26
- taskHistory: events,
27
- summary: this.generateSummary(taskId, currentState),
28
- };
29
- }
30
- /**
31
- * Record a handoff event
32
- */
33
- recordHandoff(context, result) {
34
- const path = join(this.rootDir, "jobs", context.jobId, "handoffs", `${context.taskId}.json`);
35
- const event = {
36
- ts: new Date().toISOString(),
37
- agentId: context.toAgent,
38
- action: "handoff_received",
39
- result,
40
- };
41
- context.taskHistory.push(event);
42
- writeJson(path, context);
43
- }
44
- /**
45
- * Generate handoff prompt for the receiving agent
46
- */
47
- generateHandoffPrompt(context) {
48
- const lines = [
49
- `## Agent Handoff`,
50
- ``,
51
- `**From Agent:** ${context.fromAgent}`,
52
- `**To Agent:** ${context.toAgent}`,
53
- `**Task:** ${context.taskId}`,
54
- ``,
55
- `### Task History`,
56
- ];
57
- for (const event of context.taskHistory) {
58
- lines.push(`- [${event.ts}] ${event.agentId}: ${event.action}`);
59
- if (event.result) {
60
- lines.push(` Result: ${event.result}`);
61
- }
62
- }
63
- lines.push(``);
64
- lines.push(`### Summary`);
65
- lines.push(context.summary);
66
- if (context.sharedFiles.length > 0) {
67
- lines.push(``);
68
- lines.push(`### Shared Files`);
69
- for (const file of context.sharedFiles) {
70
- lines.push(`- ${file}`);
71
- }
72
- }
73
- return lines.join("\n");
74
- }
75
- /**
76
- * Validate handoff readiness
77
- */
78
- validateHandoff(context) {
79
- const issues = [];
80
- if (!context.summary) {
81
- issues.push("Task summary is empty");
82
- }
83
- if (context.taskHistory.length === 0) {
84
- issues.push("No task history recorded");
85
- }
86
- // Check for recent handoffs
87
- const recentHandoffs = context.taskHistory.filter((h) => {
88
- const age = Date.now() - Date.parse(h.ts);
89
- return age < 5 * 60 * 1000; // 5 minutes
90
- });
91
- if (recentHandoffs.length > 3) {
92
- issues.push(`Too many recent handoffs (${recentHandoffs.length}). Possible ping-pong.`);
93
- }
94
- return { valid: issues.length === 0, issues };
95
- }
96
- /**
97
- * Load handoff history for a task
98
- */
99
- loadHandoffHistory(jobId, taskId) {
100
- const path = join(this.rootDir, "jobs", jobId, "handoffs", `${taskId}.json`);
101
- const data = readJson(path);
102
- return data?.taskHistory ?? [];
103
- }
104
- /**
105
- * Generate a summary of the task state
106
- */
107
- generateSummary(taskId, currentState) {
108
- if (!currentState) {
109
- return `Task ${taskId} requires continuation. Check task files for current state.`;
110
- }
111
- const lines = [`Task ${taskId} is in progress.`];
112
- if (currentState.filesModified) {
113
- lines.push(`Files modified: ${currentState.filesModified.join(", ")}`);
114
- }
115
- if (currentState.lastAction) {
116
- lines.push(`Last action: ${currentState.lastAction}`);
117
- }
118
- if (currentState.blockers) {
119
- lines.push(`Blockers: ${currentState.blockers}`);
120
- }
121
- return lines.join("\n");
122
- }
123
- }