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.
- package/package.json +3 -4
- package/packages/auth/src/run-minimax-auth.ts +0 -0
- package/prompts/tencentdb-connect.md +35 -0
- package/prompts/tencentdb-setup.md +101 -0
- package/prompts/tencentdb.md +88 -0
- package/.versionrc.js +0 -32
- package/cli.js +0 -112
- package/footer-status.js +0 -155
- package/harness/agent-handoff.js +0 -123
- package/harness/auto-compact.js +0 -243
- package/harness/auto-quota-resume.js +0 -104
- package/harness/blackboard.js +0 -258
- package/harness/context-compact-orchestrator.js +0 -365
- package/harness/context-window-manager.js +0 -330
- package/harness/continue-prompt.js +0 -164
- package/harness/e2e/minimax-quota-parser.js +0 -52
- package/harness/e2e/minimax-quota-scraper.js +0 -475
- package/harness/e2e/openai-quota-scraper.js +0 -332
- package/harness/e2e/playwright-runner.js +0 -165
- package/harness/e2e/quota-status.js +0 -140
- package/harness/e2e/test-engine.js +0 -290
- package/harness/forked-summarizer.js +0 -212
- package/harness/index.js +0 -54
- package/harness/job-state-machine.js +0 -277
- package/harness/loop-runtime.js +0 -531
- package/harness/master-planner.js +0 -229
- package/harness/notification-events.js +0 -233
- package/harness/output-limit-handler.js +0 -233
- package/harness/partial-recovery.js +0 -413
- package/harness/project-detector/detector.js +0 -283
- package/harness/repair-engine.js +0 -256
- package/harness/session-memory.js +0 -293
- package/harness/task-graph.js +0 -88
- package/index.js +0 -1121
- package/mirror.js +0 -205
- package/proactive-compact.js +0 -42
- package/renderer.js +0 -134
- package/status-parsers.js +0 -63
- package/tracker.js +0 -49
- 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.
|
|
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
|
|
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
|
-
}
|
package/harness/agent-handoff.js
DELETED
|
@@ -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
|
-
}
|