@holoscript/holoscript-agent 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/bin/holoscript-agent.cjs +3 -1
- package/dist/ablation.js +4 -1
- package/dist/ablation.js.map +1 -1
- package/dist/brain.js +73 -3
- package/dist/brain.js.map +1 -1
- package/dist/commit-hook.js +6 -2
- package/dist/commit-hook.js.map +1 -1
- package/dist/cost-guard.js +2 -0
- package/dist/cost-guard.js.map +1 -1
- package/dist/holomesh-client.d.ts +8 -1
- package/dist/holomesh-client.js +24 -25
- package/dist/holomesh-client.js.map +1 -1
- package/dist/identity.js +2 -2
- package/dist/identity.js.map +1 -1
- package/dist/index.js +593 -94
- package/dist/index.js.map +1 -1
- package/dist/provision.js +39 -22
- package/dist/provision.js.map +1 -1
- package/dist/runner.js +289 -20
- package/dist/runner.js.map +1 -1
- package/dist/supervisor-config.js +3 -1
- package/dist/supervisor-config.js.map +1 -1
- package/dist/supervisor.js +401 -55
- package/dist/supervisor.js.map +1 -1
- package/dist/types.d.ts +30 -2
- package/package.json +14 -11
- package/LICENSE +0 -21
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @holoscript/holoscript-agent
|
|
2
|
+
|
|
3
|
+
Headless agent runtime for the HoloMesh. Mount a `.hsplus` **brain**, point it at any
|
|
4
|
+
LLM provider (cloud, or a **local Ollama** model on an edge device), and it runs the
|
|
5
|
+
heartbeat → claim → execute loop against a HoloMesh team board — cost-guarded, identity-signed,
|
|
6
|
+
and provider-agnostic.
|
|
7
|
+
|
|
8
|
+
It is the runtime behind the sovereign **edge agent** path: a Jetson (or any Ollama host) runs
|
|
9
|
+
its brain at $0 marginal token cost via `--provider local-llm`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -g @holoscript/holoscript-agent
|
|
15
|
+
# runtime deps: @holoscript/llm-provider, ethers (both public on npm)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart — run a brain on a local model
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. point at your local Ollama + a tool-calling model
|
|
22
|
+
export HOLOSCRIPT_AGENT_PROVIDER=local-llm
|
|
23
|
+
export HOLOSCRIPT_AGENT_LOCAL_LLM_BASE_URL=http://localhost:11434 # or http://holojetson.local:11434
|
|
24
|
+
export HOLOSCRIPT_AGENT_LOCAL_LLM_MODEL=qwen3:4b-instruct # a proven tool-caller; avoid qwen2.5* (emits prose, not tool_calls)
|
|
25
|
+
|
|
26
|
+
# 2. identity (the bearer name MUST equal the agent's registered handle)
|
|
27
|
+
export HOLOSCRIPT_AGENT_HANDLE=my-edge-agent
|
|
28
|
+
export HOLOSCRIPT_AGENT_BRAIN=./my-brain.hsplus
|
|
29
|
+
export HOLOSCRIPT_AGENT_WALLET=0x…
|
|
30
|
+
export HOLOSCRIPT_AGENT_X402_BEARER=… # per-surface mesh bearer
|
|
31
|
+
export HOLOMESH_TEAM_ID=team_…
|
|
32
|
+
|
|
33
|
+
holoscript-agent whoami # verify identity resolves end-to-end
|
|
34
|
+
holoscript-agent tick # one claim+execute cycle (good for CI/cron/smoke)
|
|
35
|
+
holoscript-agent run # the daemon: heartbeat + claim + execute loop
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`run` blocks; `tick` does a single cycle and exits.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | Purpose |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `run` | start the daemon (heartbeat + claim + execute loop) |
|
|
45
|
+
| `tick` | single tick, then exit (CI / cron / smoke tests) |
|
|
46
|
+
| `whoami` | verify the identity tuple resolves (`/me` + env) |
|
|
47
|
+
| `supervise --config=<agents.json>` | run N agents from one config (multi-agent daemon) |
|
|
48
|
+
| `status --config=<path>` | print + validate a parsed supervise config |
|
|
49
|
+
| `provision --handle=<name> [--execute]` | provision a fresh x402 seat for a brain (dry-run by default) |
|
|
50
|
+
| `ablate --spec=<path> [--out-md] [--out-json]` | run a cross-LLM ablation matrix |
|
|
51
|
+
| `audit [rollup\|query\|tail]` | query the per-agent audit log |
|
|
52
|
+
| `help` | full env + flag reference |
|
|
53
|
+
|
|
54
|
+
## Providers
|
|
55
|
+
|
|
56
|
+
`HOLOSCRIPT_AGENT_PROVIDER` ∈ `anthropic | openai | gemini | xai | openrouter | local-llm | sovereign | mock`.
|
|
57
|
+
|
|
58
|
+
- **`local-llm`** — Ollama-compatible. Set `HOLOSCRIPT_AGENT_LOCAL_LLM_BASE_URL` + `HOLOSCRIPT_AGENT_LOCAL_LLM_MODEL`. Timeout defaults to 300s (edge devices are slow); override with `HOLOSCRIPT_AGENT_LOCAL_LLM_TIMEOUT_MS`.
|
|
59
|
+
- **`sovereign`** — sovereign-first auto-resolution (serving fleet → cloud → local Ollama → BYOK frontier keys); `HOLOSCRIPT_AGENT_MODEL` overrides the pick.
|
|
60
|
+
- **cloud** (`anthropic`/`openai`/`gemini`/`xai`/`openrouter`) — set the matching `*_API_KEY`.
|
|
61
|
+
|
|
62
|
+
## Environment
|
|
63
|
+
|
|
64
|
+
**Required**
|
|
65
|
+
|
|
66
|
+
| Var | Meaning |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `HOLOSCRIPT_AGENT_HANDLE` | agent handle — **must equal the registered bearer name** (else CAEL/audit POSTs 403) |
|
|
69
|
+
| `HOLOSCRIPT_AGENT_PROVIDER` | one of the providers above |
|
|
70
|
+
| `HOLOSCRIPT_AGENT_MODEL` | model id (e.g. `claude-opus-4-8`); for `local-llm` prefer `HOLOSCRIPT_AGENT_LOCAL_LLM_MODEL` |
|
|
71
|
+
| `HOLOSCRIPT_AGENT_BRAIN` | path to the `.hsplus` brain composition |
|
|
72
|
+
| `HOLOSCRIPT_AGENT_WALLET` | `0x…` wallet address |
|
|
73
|
+
| `HOLOSCRIPT_AGENT_X402_BEARER` | per-surface mesh bearer |
|
|
74
|
+
| `HOLOMESH_TEAM_ID` | target team id |
|
|
75
|
+
| `ANTHROPIC_API_KEY` \| `OPENAI_API_KEY` \| `GEMINI_API_KEY` \| … | per cloud provider |
|
|
76
|
+
|
|
77
|
+
**Optional** (defaults in parentheses) — full list via `holoscript-agent help`:
|
|
78
|
+
|
|
79
|
+
| Var | Meaning |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `HOLOSCRIPT_AGENT_LOCAL_LLM_BASE_URL` | local-llm base URL (`http://localhost:8080`) |
|
|
82
|
+
| `HOLOSCRIPT_AGENT_LOCAL_LLM_MODEL` | local-llm model id; overrides `HOLOSCRIPT_AGENT_MODEL` for the local provider |
|
|
83
|
+
| `HOLOSCRIPT_AGENT_LOCAL_LLM_TIMEOUT_MS` | local request timeout (`300000`) |
|
|
84
|
+
| `HOLOSCRIPT_AGENT_BUDGET_USD_DAY` | daily spend cap (`5`) |
|
|
85
|
+
| `HOLOSCRIPT_AGENT_TICK_MS` | daemon tick interval (`60000`) |
|
|
86
|
+
| `HOLOSCRIPT_AGENT_SCOPE_TIER` | `cold \| warm \| hot` (`warm`) |
|
|
87
|
+
| `HOLOMESH_API_BASE` | mesh API base (`https://mcp.holoscript.net/api/holomesh`) |
|
|
88
|
+
| `HOLOSCRIPT_AGENT_COMMIT_RESPONSES` | `1`/`true` → write responses as memos and git-commit them |
|
|
89
|
+
|
|
90
|
+
## Multi-agent (`supervise`)
|
|
91
|
+
|
|
92
|
+
Run a fleet from one config:
|
|
93
|
+
|
|
94
|
+
```jsonc
|
|
95
|
+
// agents.json
|
|
96
|
+
{
|
|
97
|
+
"agents": [
|
|
98
|
+
{ "handle": "edge-1", "provider": "local-llm", "model": "qwen3:4b-instruct", "brain": "./brains/edge.hsplus" },
|
|
99
|
+
{ "handle": "planner", "provider": "sovereign", "brain": "./brains/planner.hsplus" }
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
holoscript-agent status --config=agents.json # validate first
|
|
106
|
+
holoscript-agent supervise --config=agents.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Subpath exports
|
|
110
|
+
|
|
111
|
+
`@holoscript/holoscript-agent` and its subpaths: `./runner`, `./brain`, `./cost-guard`,
|
|
112
|
+
`./identity`, `./commit-hook`, `./ablation`, `./supervisor`, `./supervisor-config`,
|
|
113
|
+
`./provision`, `./audit-log` — for embedding the runtime instead of using the CLI.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/bin/holoscript-agent.cjs
CHANGED
|
@@ -6,7 +6,9 @@ const { pathToFileURL } = require('node:url');
|
|
|
6
6
|
const target = join(__dirname, '..', 'dist', 'index.js');
|
|
7
7
|
|
|
8
8
|
if (!existsSync(target)) {
|
|
9
|
-
console.error(
|
|
9
|
+
console.error(
|
|
10
|
+
'[holoscript-agent] build output is missing. Run `pnpm --filter @holoscript/holoscript-agent run build` first.'
|
|
11
|
+
);
|
|
10
12
|
process.exit(1);
|
|
11
13
|
}
|
|
12
14
|
|
package/dist/ablation.js
CHANGED
|
@@ -157,7 +157,10 @@ USR:${user}`).digest("hex").slice(0, 16);
|
|
|
157
157
|
}
|
|
158
158
|
function withTimeout(p, ms, label) {
|
|
159
159
|
return new Promise((resolve, reject) => {
|
|
160
|
-
const timer = setTimeout(
|
|
160
|
+
const timer = setTimeout(
|
|
161
|
+
() => reject(new Error(`ablation cell "${label}" timed out after ${ms}ms`)),
|
|
162
|
+
ms
|
|
163
|
+
);
|
|
161
164
|
p.then(
|
|
162
165
|
(v) => {
|
|
163
166
|
clearTimeout(timer);
|
package/dist/ablation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ablation.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type {\n ILLMProvider,\n LLMCompletionRequest,\n LLMProviderName,\n TokenUsage,\n} from '@holoscript/llm-provider';\nimport type { CostGuard } from './cost-guard.js';\nimport type { AuditLog } from './audit-log.js';\nimport type { AgentIdentity } from './types.js';\n\nexport interface AblationProviderSpec {\n label: string;\n provider: LLMProviderName;\n model: string;\n build: () => Promise<ILLMProvider> | ILLMProvider;\n pricer?: (usage: TokenUsage) => number;\n}\n\nexport interface AblationTaskSpec {\n taskId: string;\n taskTitle: string;\n systemPrompt: string;\n userPrompt: string;\n brainPath?: string;\n maxTokens?: number;\n temperature?: number;\n}\n\nexport interface AblationCell {\n label: string;\n provider: LLMProviderName;\n model: string;\n responseText: string;\n usage: TokenUsage;\n costUsd: number;\n durationMs: number;\n finishReason: string;\n errorMessage?: string;\n}\n\nexport interface AblationMatrix {\n taskId: string;\n taskTitle: string;\n brainPath?: string;\n promptHash: string;\n cells: AblationCell[];\n totalCostUsd: number;\n startedAt: string;\n completedAt: string;\n budgetExhausted: boolean;\n}\n\nexport interface RunAblationOptions {\n task: AblationTaskSpec;\n providers: AblationProviderSpec[];\n costGuard?: CostGuard;\n timeoutPerCellMs?: number;\n auditLog?: AuditLog;\n matrixId?: string;\n identityFor?: (spec: AblationProviderSpec) => AgentIdentity;\n}\n\nexport async function runAblation(opts: RunAblationOptions): Promise<AblationMatrix> {\n const { task, providers, costGuard } = opts;\n const startedAt = new Date().toISOString();\n const promptHash = hashPrompt(task.systemPrompt, task.userPrompt);\n\n const request: LLMCompletionRequest = {\n messages: [\n { role: 'system', content: task.systemPrompt },\n { role: 'user', content: task.userPrompt },\n ],\n maxTokens: task.maxTokens ?? 2048,\n temperature: task.temperature ?? 0.4,\n };\n\n const cells: AblationCell[] = [];\n let budgetExhausted = false;\n const matrixId = opts.matrixId ?? `mx_${promptHash}_${Date.now()}`;\n\n for (const spec of providers) {\n if (costGuard?.isOverBudget()) {\n budgetExhausted = true;\n cells.push({\n label: spec.label,\n provider: spec.provider,\n model: spec.model,\n responseText: '',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n costUsd: 0,\n durationMs: 0,\n finishReason: 'error',\n errorMessage: 'budget-exhausted-before-cell',\n });\n continue;\n }\n\n const t0 = Date.now();\n try {\n const provider = await spec.build();\n const cellPromise = provider.complete(request, spec.model);\n const response = opts.timeoutPerCellMs\n ? await withTimeout(cellPromise, opts.timeoutPerCellMs, spec.label)\n : await cellPromise;\n const durationMs = Date.now() - t0;\n\n const costUsd = spec.pricer\n ? spec.pricer(response.usage)\n : costGuard?.recordUsage(spec.model, response.usage).costUsd ?? 0;\n if (spec.pricer && costGuard) {\n costGuard.recordUsage(spec.model, response.usage);\n }\n\n cells.push({\n label: spec.label,\n provider: spec.provider,\n model: spec.model,\n responseText: response.content,\n usage: response.usage,\n costUsd,\n durationMs,\n finishReason: response.finishReason,\n });\n recordAblationCellIfWired(opts, spec, {\n matrixId,\n promptHash,\n promptTokens: response.usage.promptTokens,\n completionTokens: response.usage.completionTokens,\n costUsd,\n durationMs,\n finishReason: response.finishReason,\n });\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n cells.push({\n label: spec.label,\n provider: spec.provider,\n model: spec.model,\n responseText: '',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n costUsd: 0,\n durationMs: Date.now() - t0,\n finishReason: 'error',\n errorMessage,\n });\n recordAblationCellIfWired(opts, spec, {\n matrixId,\n promptHash,\n promptTokens: 0,\n completionTokens: 0,\n costUsd: 0,\n durationMs: Date.now() - t0,\n finishReason: 'error',\n errorMessage,\n });\n }\n }\n\n return {\n taskId: task.taskId,\n taskTitle: task.taskTitle,\n brainPath: task.brainPath,\n promptHash,\n cells,\n totalCostUsd: cells.reduce((sum, c) => sum + c.costUsd, 0),\n startedAt,\n completedAt: new Date().toISOString(),\n budgetExhausted,\n };\n}\n\nexport function renderAblationMarkdown(matrix: AblationMatrix): string {\n const header = [\n `# Ablation: ${matrix.taskTitle}`,\n '',\n `- task_id: \\`${matrix.taskId}\\``,\n `- prompt_hash: \\`${matrix.promptHash}\\``,\n matrix.brainPath ? `- brain: \\`${matrix.brainPath}\\`` : '- brain: _(none)_',\n `- started: ${matrix.startedAt}`,\n `- completed: ${matrix.completedAt}`,\n `- total_cost_usd: $${matrix.totalCostUsd.toFixed(4)}`,\n matrix.budgetExhausted ? `- **budget_exhausted: true** (some cells skipped)` : '',\n '',\n '| Label | Provider | Model | Tokens (in/out) | Cost (USD) | Duration (ms) | Finish | Excerpt |',\n '|---|---|---|---|---|---|---|---|',\n ].filter((line) => line !== '');\n\n const rows = matrix.cells.map((c) => {\n const tokens = `${c.usage.promptTokens}/${c.usage.completionTokens}`;\n const excerpt = c.errorMessage\n ? `_error: ${truncate(c.errorMessage, 80)}_`\n : truncate(escapeMd(c.responseText.replace(/\\n/g, ' ')), 80);\n return `| ${c.label} | ${c.provider} | ${c.model} | ${tokens} | $${c.costUsd.toFixed(4)} | ${c.durationMs} | ${c.finishReason} | ${excerpt} |`;\n });\n\n return [...header, ...rows, ''].join('\\n');\n}\n\nfunction recordAblationCellIfWired(\n opts: RunAblationOptions,\n spec: AblationProviderSpec,\n cell: {\n matrixId: string;\n promptHash: string;\n promptTokens: number;\n completionTokens: number;\n costUsd: number;\n durationMs: number;\n finishReason: string;\n errorMessage?: string;\n }\n): void {\n if (!opts.auditLog) return;\n const identity = opts.identityFor?.(spec) ?? {\n handle: `ablation:${spec.label}`,\n surface: `ablation:${spec.label}`,\n wallet: '0x0000000000000000000000000000000000000000',\n x402Bearer: '',\n llmProvider: spec.provider,\n llmModel: spec.model,\n brainPath: opts.task.brainPath ?? '(none)',\n budgetUsdPerDay: 0,\n teamId: '(ablation)',\n meshApiBase: '(ablation)',\n };\n try {\n opts.auditLog.recordAblationCell({\n identity,\n matrixId: cell.matrixId,\n label: spec.label,\n taskId: opts.task.taskId,\n taskTitle: opts.task.taskTitle,\n promptHash: cell.promptHash,\n promptTokens: cell.promptTokens,\n completionTokens: cell.completionTokens,\n costUsd: cell.costUsd,\n durationMs: cell.durationMs,\n finishReason: cell.finishReason,\n errorMessage: cell.errorMessage,\n });\n } catch {\n // Audit log write must never break the ablation matrix output.\n }\n}\n\nfunction hashPrompt(system: string, user: string): string {\n return createHash('sha256').update(`SYS:${system}\\nUSR:${user}`).digest('hex').slice(0, 16);\n}\n\nfunction withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`ablation cell \"${label}\" timed out after ${ms}ms`)), ms);\n p.then(\n (v) => {\n clearTimeout(timer);\n resolve(v);\n },\n (e) => {\n clearTimeout(timer);\n reject(e);\n }\n );\n });\n}\n\nfunction truncate(s: string, max: number): string {\n return s.length <= max ? s : `${s.slice(0, max - 1)}…`;\n}\n\nfunction escapeMd(s: string): string {\n return s.replace(/\\|/g, '\\\\|');\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/ablation.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type {\n ILLMProvider,\n LLMCompletionRequest,\n LLMProviderName,\n TokenUsage,\n} from '@holoscript/llm-provider';\nimport type { CostGuard } from './cost-guard.js';\nimport type { AuditLog } from './audit-log.js';\nimport type { AgentIdentity } from './types.js';\n\nexport interface AblationProviderSpec {\n label: string;\n provider: LLMProviderName;\n model: string;\n build: () => Promise<ILLMProvider> | ILLMProvider;\n pricer?: (usage: TokenUsage) => number;\n}\n\nexport interface AblationTaskSpec {\n taskId: string;\n taskTitle: string;\n systemPrompt: string;\n userPrompt: string;\n brainPath?: string;\n maxTokens?: number;\n temperature?: number;\n}\n\nexport interface AblationCell {\n label: string;\n provider: LLMProviderName;\n model: string;\n responseText: string;\n usage: TokenUsage;\n costUsd: number;\n durationMs: number;\n finishReason: string;\n errorMessage?: string;\n}\n\nexport interface AblationMatrix {\n taskId: string;\n taskTitle: string;\n brainPath?: string;\n promptHash: string;\n cells: AblationCell[];\n totalCostUsd: number;\n startedAt: string;\n completedAt: string;\n budgetExhausted: boolean;\n}\n\nexport interface RunAblationOptions {\n task: AblationTaskSpec;\n providers: AblationProviderSpec[];\n costGuard?: CostGuard;\n timeoutPerCellMs?: number;\n auditLog?: AuditLog;\n matrixId?: string;\n identityFor?: (spec: AblationProviderSpec) => AgentIdentity;\n}\n\nexport async function runAblation(opts: RunAblationOptions): Promise<AblationMatrix> {\n const { task, providers, costGuard } = opts;\n const startedAt = new Date().toISOString();\n const promptHash = hashPrompt(task.systemPrompt, task.userPrompt);\n\n const request: LLMCompletionRequest = {\n messages: [\n { role: 'system', content: task.systemPrompt },\n { role: 'user', content: task.userPrompt },\n ],\n maxTokens: task.maxTokens ?? 2048,\n temperature: task.temperature ?? 0.4,\n };\n\n const cells: AblationCell[] = [];\n let budgetExhausted = false;\n const matrixId = opts.matrixId ?? `mx_${promptHash}_${Date.now()}`;\n\n for (const spec of providers) {\n if (costGuard?.isOverBudget()) {\n budgetExhausted = true;\n cells.push({\n label: spec.label,\n provider: spec.provider,\n model: spec.model,\n responseText: '',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n costUsd: 0,\n durationMs: 0,\n finishReason: 'error',\n errorMessage: 'budget-exhausted-before-cell',\n });\n continue;\n }\n\n const t0 = Date.now();\n try {\n const provider = await spec.build();\n const cellPromise = provider.complete(request, spec.model);\n const response = opts.timeoutPerCellMs\n ? await withTimeout(cellPromise, opts.timeoutPerCellMs, spec.label)\n : await cellPromise;\n const durationMs = Date.now() - t0;\n\n const costUsd = spec.pricer\n ? spec.pricer(response.usage)\n : (costGuard?.recordUsage(spec.model, response.usage).costUsd ?? 0);\n if (spec.pricer && costGuard) {\n costGuard.recordUsage(spec.model, response.usage);\n }\n\n cells.push({\n label: spec.label,\n provider: spec.provider,\n model: spec.model,\n responseText: response.content,\n usage: response.usage,\n costUsd,\n durationMs,\n finishReason: response.finishReason,\n });\n recordAblationCellIfWired(opts, spec, {\n matrixId,\n promptHash,\n promptTokens: response.usage.promptTokens,\n completionTokens: response.usage.completionTokens,\n costUsd,\n durationMs,\n finishReason: response.finishReason,\n });\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n cells.push({\n label: spec.label,\n provider: spec.provider,\n model: spec.model,\n responseText: '',\n usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },\n costUsd: 0,\n durationMs: Date.now() - t0,\n finishReason: 'error',\n errorMessage,\n });\n recordAblationCellIfWired(opts, spec, {\n matrixId,\n promptHash,\n promptTokens: 0,\n completionTokens: 0,\n costUsd: 0,\n durationMs: Date.now() - t0,\n finishReason: 'error',\n errorMessage,\n });\n }\n }\n\n return {\n taskId: task.taskId,\n taskTitle: task.taskTitle,\n brainPath: task.brainPath,\n promptHash,\n cells,\n totalCostUsd: cells.reduce((sum, c) => sum + c.costUsd, 0),\n startedAt,\n completedAt: new Date().toISOString(),\n budgetExhausted,\n };\n}\n\nexport function renderAblationMarkdown(matrix: AblationMatrix): string {\n const header = [\n `# Ablation: ${matrix.taskTitle}`,\n '',\n `- task_id: \\`${matrix.taskId}\\``,\n `- prompt_hash: \\`${matrix.promptHash}\\``,\n matrix.brainPath ? `- brain: \\`${matrix.brainPath}\\`` : '- brain: _(none)_',\n `- started: ${matrix.startedAt}`,\n `- completed: ${matrix.completedAt}`,\n `- total_cost_usd: $${matrix.totalCostUsd.toFixed(4)}`,\n matrix.budgetExhausted ? `- **budget_exhausted: true** (some cells skipped)` : '',\n '',\n '| Label | Provider | Model | Tokens (in/out) | Cost (USD) | Duration (ms) | Finish | Excerpt |',\n '|---|---|---|---|---|---|---|---|',\n ].filter((line) => line !== '');\n\n const rows = matrix.cells.map((c) => {\n const tokens = `${c.usage.promptTokens}/${c.usage.completionTokens}`;\n const excerpt = c.errorMessage\n ? `_error: ${truncate(c.errorMessage, 80)}_`\n : truncate(escapeMd(c.responseText.replace(/\\n/g, ' ')), 80);\n return `| ${c.label} | ${c.provider} | ${c.model} | ${tokens} | $${c.costUsd.toFixed(4)} | ${c.durationMs} | ${c.finishReason} | ${excerpt} |`;\n });\n\n return [...header, ...rows, ''].join('\\n');\n}\n\nfunction recordAblationCellIfWired(\n opts: RunAblationOptions,\n spec: AblationProviderSpec,\n cell: {\n matrixId: string;\n promptHash: string;\n promptTokens: number;\n completionTokens: number;\n costUsd: number;\n durationMs: number;\n finishReason: string;\n errorMessage?: string;\n }\n): void {\n if (!opts.auditLog) return;\n const identity = opts.identityFor?.(spec) ?? {\n handle: `ablation:${spec.label}`,\n surface: `ablation:${spec.label}`,\n wallet: '0x0000000000000000000000000000000000000000',\n x402Bearer: '',\n llmProvider: spec.provider,\n llmModel: spec.model,\n brainPath: opts.task.brainPath ?? '(none)',\n budgetUsdPerDay: 0,\n teamId: '(ablation)',\n meshApiBase: '(ablation)',\n };\n try {\n opts.auditLog.recordAblationCell({\n identity,\n matrixId: cell.matrixId,\n label: spec.label,\n taskId: opts.task.taskId,\n taskTitle: opts.task.taskTitle,\n promptHash: cell.promptHash,\n promptTokens: cell.promptTokens,\n completionTokens: cell.completionTokens,\n costUsd: cell.costUsd,\n durationMs: cell.durationMs,\n finishReason: cell.finishReason,\n errorMessage: cell.errorMessage,\n });\n } catch {\n // Audit log write must never break the ablation matrix output.\n }\n}\n\nfunction hashPrompt(system: string, user: string): string {\n return createHash('sha256').update(`SYS:${system}\\nUSR:${user}`).digest('hex').slice(0, 16);\n}\n\nfunction withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new Error(`ablation cell \"${label}\" timed out after ${ms}ms`)),\n ms\n );\n p.then(\n (v) => {\n clearTimeout(timer);\n resolve(v);\n },\n (e) => {\n clearTimeout(timer);\n reject(e);\n }\n );\n });\n}\n\nfunction truncate(s: string, max: number): string {\n return s.length <= max ? s : `${s.slice(0, max - 1)}…`;\n}\n\nfunction escapeMd(s: string): string {\n return s.replace(/\\|/g, '\\\\|');\n}\n"],"mappings":"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","names":[]}
|
package/dist/brain.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/brain.ts
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
async function loadBrain(brainPath, scopeTier = "warm") {
|
|
4
|
-
const
|
|
5
|
-
const { domain, capabilityTags, requires, prefers, avoids } = extractIdentity(
|
|
4
|
+
const raw = await readFile(brainPath, "utf8");
|
|
5
|
+
const { domain, capabilityTags, requires, prefers, avoids } = extractIdentity(raw);
|
|
6
|
+
const systemPrompt = extractSystemPromptPreamble(raw);
|
|
6
7
|
return {
|
|
7
8
|
brainPath,
|
|
8
9
|
systemPrompt,
|
|
@@ -11,9 +12,31 @@ async function loadBrain(brainPath, scopeTier = "warm") {
|
|
|
11
12
|
scopeTier,
|
|
12
13
|
requires,
|
|
13
14
|
prefers,
|
|
14
|
-
avoids
|
|
15
|
+
avoids,
|
|
16
|
+
reflect: extractReflect(raw),
|
|
17
|
+
onTaskActions: extractOnTaskActions(raw)
|
|
15
18
|
};
|
|
16
19
|
}
|
|
20
|
+
function extractReflect(brain) {
|
|
21
|
+
const block = sliceNamedBlock(brain, "reflect");
|
|
22
|
+
if (block === void 0) return void 0;
|
|
23
|
+
const criteria = scalarField(block, "criteria") ?? scalarField(block, "scorer") ?? scalarField(block, "of") ?? "correctness, completeness, and valid HoloScript syntax";
|
|
24
|
+
const escRaw = scalarField(block, "escalate_on_fail") ?? scalarField(block, "escalateOnFail") ?? scalarField(block, "escalate");
|
|
25
|
+
return { criteria, escalateOnFail: (escRaw ?? "").split(",")[0].trim().toLowerCase() === "true" };
|
|
26
|
+
}
|
|
27
|
+
function extractSystemPromptPreamble(src) {
|
|
28
|
+
const lines = src.split("\n");
|
|
29
|
+
const BLOCK_START = /^(#version|#target|#mode|identity\s*\{|state\s*\{|computed\s*\{|traits\s*\[|capabilities\s*\{|directives\s*\{|behavior\s)/;
|
|
30
|
+
let cutLine = -1;
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
if (BLOCK_START.test(lines[i].trim())) {
|
|
33
|
+
cutLine = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (cutLine <= 0) return src;
|
|
38
|
+
return lines.slice(0, cutLine).join("\n").trimEnd();
|
|
39
|
+
}
|
|
17
40
|
function extractIdentity(brain) {
|
|
18
41
|
const identityBlock = sliceNamedBlock(brain, "identity");
|
|
19
42
|
if (!identityBlock) {
|
|
@@ -26,6 +49,53 @@ function extractIdentity(brain) {
|
|
|
26
49
|
const avoids = listField(identityBlock, "avoids") ?? [];
|
|
27
50
|
return { domain, capabilityTags, requires, prefers, avoids };
|
|
28
51
|
}
|
|
52
|
+
function extractOnTaskActions(brain) {
|
|
53
|
+
const block = sliceNamedBlock(brain, "on_task");
|
|
54
|
+
if (!block) return [];
|
|
55
|
+
const VERBS = ["recall", "rag_query", "llm_call", "plan", "reflect"];
|
|
56
|
+
const entries = [];
|
|
57
|
+
for (const verb of VERBS) {
|
|
58
|
+
const re = new RegExp(`\\b${verb}\\s*\\{`, "g");
|
|
59
|
+
let m;
|
|
60
|
+
while ((m = re.exec(block)) !== null) {
|
|
61
|
+
const start = m.index + m[0].length;
|
|
62
|
+
let depth = 1;
|
|
63
|
+
let end = -1;
|
|
64
|
+
for (let i = start; i < block.length; i++) {
|
|
65
|
+
if (block[i] === "{") depth++;
|
|
66
|
+
else if (block[i] === "}") {
|
|
67
|
+
depth--;
|
|
68
|
+
if (depth === 0) {
|
|
69
|
+
end = i;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (end < 0) continue;
|
|
75
|
+
entries.push({ verb, config: parseKVBlock(block.slice(start, end)), _pos: m.index });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return entries.sort((a, b) => a._pos - b._pos).map(({ _pos: _ignored, ...rest }) => rest);
|
|
79
|
+
}
|
|
80
|
+
function parseKVBlock(block) {
|
|
81
|
+
const out = {};
|
|
82
|
+
const strRe = /\b(\w+)\s*:\s*"([^"]*)"/g;
|
|
83
|
+
let m;
|
|
84
|
+
while ((m = strRe.exec(block)) !== null) out[m[1]] = m[2];
|
|
85
|
+
const arrRe = /\b(\w+)\s*:\s*\[([^\]]*)\]/g;
|
|
86
|
+
while ((m = arrRe.exec(block)) !== null) {
|
|
87
|
+
out[m[1]] = m[2].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
|
|
88
|
+
}
|
|
89
|
+
const boolRe = /\b(\w+)\s*:\s*(true|false)\b/g;
|
|
90
|
+
while ((m = boolRe.exec(block)) !== null) {
|
|
91
|
+
if (!(m[1] in out)) out[m[1]] = m[2] === "true";
|
|
92
|
+
}
|
|
93
|
+
const numRe = /\b(\w+)\s*:\s*(-?\d+(?:\.\d+)?)\b/g;
|
|
94
|
+
while ((m = numRe.exec(block)) !== null) {
|
|
95
|
+
if (!(m[1] in out)) out[m[1]] = parseFloat(m[2]);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
29
99
|
function sliceNamedBlock(src, name) {
|
|
30
100
|
const re = new RegExp(`\\b${name}\\s*:?\\s*\\{`, "g");
|
|
31
101
|
const match = re.exec(src);
|
package/dist/brain.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/brain.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport type { RuntimeBrainConfig } from './types.js';\n\nexport async function loadBrain(\n brainPath: string,\n scopeTier: 'cold' | 'warm' | 'hot' = 'warm'\n): Promise<RuntimeBrainConfig> {\n const systemPrompt = await readFile(brainPath, 'utf8');\n const { domain, capabilityTags, requires, prefers, avoids } = extractIdentity(systemPrompt);\n return {\n brainPath,\n systemPrompt,\n capabilityTags,\n domain,\n scopeTier,\n requires,\n prefers,\n avoids,\n };\n}\n\nfunction extractIdentity(brain: string): {\n domain: string;\n capabilityTags: string[];\n requires: string[];\n prefers: string[];\n avoids: string[];\n} {\n const identityBlock = sliceNamedBlock(brain, 'identity');\n if (!identityBlock) {\n // No identity block — open routing (backward-compatible default).\n // Brains without explicit requires/prefers/avoids match any provider.\n return { domain: 'unknown', capabilityTags: [], requires: [], prefers: [], avoids: [] };\n }\n const domain = scalarField(identityBlock, 'domain') ?? 'unknown';\n const capabilityTags = listField(identityBlock, 'capability_tags') ?? [];\n // Universal+segregated routing fields (founder ruling 2026-05-06): brains\n // declare capability requirements as data; router matches against the\n // provider's `capabilities` manifest at session start. Empty (omitted) =\n // open routing — preserves today's behavior for unmigrated brains.\n const requires = listField(identityBlock, 'requires') ?? [];\n const prefers = listField(identityBlock, 'prefers') ?? [];\n const avoids = listField(identityBlock, 'avoids') ?? [];\n return { domain, capabilityTags, requires, prefers, avoids };\n}\n\nfunction sliceNamedBlock(src: string, name: string): string | undefined {\n // Accept both `identity {` and `identity: {` — brain compositions in\n // .ai-ecosystem use both forms (lean-theorist + antigravity-hot use the\n // colon variant; security-auditor + others use the bare form). Without\n // both-form tolerance the colon-form brains parse to empty\n // capability_tags, breaking task scoring entirely (silent claim-blackhole\n // observed 2026-04-25 on W01 H200 lean-theorist).\n const re = new RegExp(`\\\\b${name}\\\\s*:?\\\\s*\\\\{`, 'g');\n const match = re.exec(src);\n if (!match) return undefined;\n const headerEnd = match.index + match[0].length; // position just past the `{`\n let depth = 1;\n for (let i = headerEnd; i < src.length; i++) {\n const ch = src[i];\n if (ch === '{') depth++;\n else if (ch === '}') {\n depth--;\n if (depth === 0) return src.slice(headerEnd, i);\n }\n }\n return undefined;\n}\n\nfunction scalarField(block: string, key: string): string | undefined {\n const idx = block.indexOf(`${key}:`);\n if (idx < 0) return undefined;\n const after = block.slice(idx + key.length + 1).trimStart();\n if (after.startsWith('\"')) {\n const end = after.indexOf('\"', 1);\n if (end > 0) return after.slice(1, end);\n }\n const eol = after.indexOf('\\n');\n return after.slice(0, eol < 0 ? undefined : eol).trim();\n}\n\nfunction listField(block: string, key: string): string[] | undefined {\n const idx = block.indexOf(`${key}:`);\n if (idx < 0) return undefined;\n const after = block.slice(idx + key.length + 1).trimStart();\n if (!after.startsWith('[')) return undefined;\n let depth = 0;\n let end = -1;\n for (let i = 0; i < after.length; i++) {\n if (after[i] === '[') depth++;\n else if (after[i] === ']') {\n depth--;\n if (depth === 0) {\n end = i;\n break;\n }\n }\n }\n if (end < 0) return undefined;\n const inner = after.slice(1, end);\n return inner\n .split(',')\n .map((s) => s.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter((s) => s.length > 0);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/brain.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport type { OnTaskAction, RuntimeBrainConfig } from './types.js';\n\nexport async function loadBrain(\n brainPath: string,\n scopeTier: 'cold' | 'warm' | 'hot' = 'warm'\n): Promise<RuntimeBrainConfig> {\n const raw = await readFile(brainPath, 'utf8');\n const { domain, capabilityTags, requires, prefers, avoids } = extractIdentity(raw);\n // For .hsplus brains: the file begins with a free-text instruction block\n // (the actual system prompt for the LLM) followed by HoloScript structured\n // sections (#version, #target, identity {}, state {}, etc.). Sending the\n // full file bloats the context by ~1500+ tokens of metadata the LLM does\n // not need and — on constrained-context local models (qwen3:4b, num_ctx=2048)\n // — causes the CRITICAL tool-calling rules to be truncated before the model\n // sees them, resulting in plain-text replies with no tool calls.\n // Extract only the preamble: everything before the first HoloScript directive.\n const systemPrompt = extractSystemPromptPreamble(raw);\n return {\n brainPath,\n systemPrompt,\n capabilityTags,\n domain,\n scopeTier,\n requires,\n prefers,\n avoids,\n reflect: extractReflect(raw),\n onTaskActions: extractOnTaskActions(raw),\n };\n}\n\n/**\n * Extract the brain's `reflect` cognitive verb (W.736) if it declares one, e.g.\n * reflect { of: \"the produced artifact\", criteria: \"valid HoloScript\", escalate_on_fail: true }\n * Returns the evaluation criteria + whether a failed self-evaluation escalates to\n * the fleet (the `local_first` directive). Absent → undefined (no reflect gate).\n * Uses sliceNamedBlock so both `reflect {` and `reflect: {` forms parse, mirroring\n * identity. This is the one cognitive verb the lightweight runner can execute with\n * just its LLM provider (no engine/trait runtime) — recall/rag_query/plan need\n * trait-backed stores and run in the core/engine path, not here.\n */\nfunction extractReflect(brain: string): { criteria: string; escalateOnFail: boolean } | undefined {\n const block = sliceNamedBlock(brain, 'reflect');\n if (block === undefined) return undefined;\n const criteria =\n scalarField(block, 'criteria') ??\n scalarField(block, 'scorer') ??\n scalarField(block, 'of') ??\n 'correctness, completeness, and valid HoloScript syntax';\n const escRaw =\n scalarField(block, 'escalate_on_fail') ??\n scalarField(block, 'escalateOnFail') ??\n scalarField(block, 'escalate');\n // escRaw may be `true` or `true, nextField...` (unquoted scalar runs to the\n // segment end) — take the first comma-delimited token before comparing.\n return { criteria, escalateOnFail: (escRaw ?? '').split(',')[0].trim().toLowerCase() === 'true' };\n}\n\n/**\n * Extract the free-text instruction preamble from a .hsplus brain file.\n * Stops at the first line that begins a HoloScript structured section:\n * `#version`, `#target`, `#mode`, or a block keyword (`identity {`,\n * `state {`, `computed {`, `traits [`, `capabilities {`, `directives {`,\n * `behavior `). Falls back to the full file content for plain-text brains\n * (no HoloScript sections detected).\n */\nfunction extractSystemPromptPreamble(src: string): string {\n const lines = src.split('\\n');\n const BLOCK_START = /^(#version|#target|#mode|identity\\s*\\{|state\\s*\\{|computed\\s*\\{|traits\\s*\\[|capabilities\\s*\\{|directives\\s*\\{|behavior\\s)/;\n let cutLine = -1;\n for (let i = 0; i < lines.length; i++) {\n if (BLOCK_START.test(lines[i].trim())) {\n cutLine = i;\n break;\n }\n }\n if (cutLine <= 0) return src; // no HoloScript sections — whole file is prompt\n return lines.slice(0, cutLine).join('\\n').trimEnd();\n}\n\nfunction extractIdentity(brain: string): {\n domain: string;\n capabilityTags: string[];\n requires: string[];\n prefers: string[];\n avoids: string[];\n} {\n const identityBlock = sliceNamedBlock(brain, 'identity');\n if (!identityBlock) {\n // No identity block — open routing (backward-compatible default).\n // Brains without explicit requires/prefers/avoids match any provider.\n return { domain: 'unknown', capabilityTags: [], requires: [], prefers: [], avoids: [] };\n }\n const domain = scalarField(identityBlock, 'domain') ?? 'unknown';\n const capabilityTags = listField(identityBlock, 'capability_tags') ?? [];\n // Universal+segregated routing fields (founder ruling 2026-05-06): brains\n // declare capability requirements as data; router matches against the\n // provider's `capabilities` manifest at session start. Empty (omitted) =\n // open routing — preserves today's behavior for unmigrated brains.\n const requires = listField(identityBlock, 'requires') ?? [];\n const prefers = listField(identityBlock, 'prefers') ?? [];\n const avoids = listField(identityBlock, 'avoids') ?? [];\n return { domain, capabilityTags, requires, prefers, avoids };\n}\n\n/**\n * Parse the `behavior on_task { … }` block into an ordered sequence of\n * cognitive verb calls (Phase 2.1). Each verb's config is extracted with a\n * lightweight regex KV parser — no full parser dependency. Only verbs whose\n * keys match known cognitive verbs are included; unknown keywords are skipped.\n *\n * Currently wired in AgentRunner: `llm_call` (prompt augmentation) and\n * `reflect` (extracted separately by extractReflect via sliceNamedBlock).\n * Future verbs (`recall`, `rag_query`, `plan`) are parsed so they appear in\n * the returned sequence but are logged-and-deferred by the runner until\n * Phase 2.2 (trait-backed stores, see idea-seeds.md).\n */\nfunction extractOnTaskActions(brain: string): OnTaskAction[] {\n // `sliceNamedBlock` with 'on_task' matches `on_task {` inside `behavior on_task {`\n const block = sliceNamedBlock(brain, 'on_task');\n if (!block) return [];\n\n const VERBS: OnTaskAction['verb'][] = ['recall', 'rag_query', 'llm_call', 'plan', 'reflect'];\n const entries: Array<OnTaskAction & { _pos: number }> = [];\n\n for (const verb of VERBS) {\n const re = new RegExp(`\\\\b${verb}\\\\s*\\\\{`, 'g');\n let m: RegExpExecArray | null;\n while ((m = re.exec(block)) !== null) {\n const start = m.index + m[0].length;\n let depth = 1;\n let end = -1;\n for (let i = start; i < block.length; i++) {\n if (block[i] === '{') depth++;\n else if (block[i] === '}') {\n depth--;\n if (depth === 0) {\n end = i;\n break;\n }\n }\n }\n if (end < 0) continue;\n entries.push({ verb, config: parseKVBlock(block.slice(start, end)), _pos: m.index });\n }\n }\n\n // Sort by authored position so verbs execute in the order the brain declared them.\n return entries.sort((a, b) => a._pos - b._pos).map(({ _pos: _ignored, ...rest }) => rest);\n}\n\n/** Lightweight key-value extractor for cognitive verb config blocks. */\nfunction parseKVBlock(block: string): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n // String: key: \"value\"\n const strRe = /\\b(\\w+)\\s*:\\s*\"([^\"]*)\"/g;\n let m: RegExpExecArray | null;\n while ((m = strRe.exec(block)) !== null) out[m[1]] = m[2];\n // Array: key: [\"a\", \"b\"] — must run before bool/num to claim the array form of limit etc.\n const arrRe = /\\b(\\w+)\\s*:\\s*\\[([^\\]]*)\\]/g;\n while ((m = arrRe.exec(block)) !== null) {\n out[m[1]] = m[2]\n .split(',')\n .map((s) => s.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter((s) => s.length > 0);\n }\n // Boolean: key: true | false (only when not already set by string/array)\n const boolRe = /\\b(\\w+)\\s*:\\s*(true|false)\\b/g;\n while ((m = boolRe.exec(block)) !== null) {\n if (!(m[1] in out)) out[m[1]] = m[2] === 'true';\n }\n // Number: key: 123 or key: -0.5 (only when not already set)\n const numRe = /\\b(\\w+)\\s*:\\s*(-?\\d+(?:\\.\\d+)?)\\b/g;\n while ((m = numRe.exec(block)) !== null) {\n if (!(m[1] in out)) out[m[1]] = parseFloat(m[2]);\n }\n return out;\n}\n\nfunction sliceNamedBlock(src: string, name: string): string | undefined {\n // Accept both `identity {` and `identity: {` — brain compositions in\n // .ai-ecosystem use both forms (lean-theorist + antigravity-hot use the\n // colon variant; security-auditor + others use the bare form). Without\n // both-form tolerance the colon-form brains parse to empty\n // capability_tags, breaking task scoring entirely (silent claim-blackhole\n // observed 2026-04-25 on W01 H200 lean-theorist).\n const re = new RegExp(`\\\\b${name}\\\\s*:?\\\\s*\\\\{`, 'g');\n const match = re.exec(src);\n if (!match) return undefined;\n const headerEnd = match.index + match[0].length; // position just past the `{`\n let depth = 1;\n for (let i = headerEnd; i < src.length; i++) {\n const ch = src[i];\n if (ch === '{') depth++;\n else if (ch === '}') {\n depth--;\n if (depth === 0) return src.slice(headerEnd, i);\n }\n }\n return undefined;\n}\n\nfunction scalarField(block: string, key: string): string | undefined {\n const idx = block.indexOf(`${key}:`);\n if (idx < 0) return undefined;\n const after = block.slice(idx + key.length + 1).trimStart();\n if (after.startsWith('\"')) {\n const end = after.indexOf('\"', 1);\n if (end > 0) return after.slice(1, end);\n }\n const eol = after.indexOf('\\n');\n return after.slice(0, eol < 0 ? undefined : eol).trim();\n}\n\nfunction listField(block: string, key: string): string[] | undefined {\n const idx = block.indexOf(`${key}:`);\n if (idx < 0) return undefined;\n const after = block.slice(idx + key.length + 1).trimStart();\n if (!after.startsWith('[')) return undefined;\n let depth = 0;\n let end = -1;\n for (let i = 0; i < after.length; i++) {\n if (after[i] === '[') depth++;\n else if (after[i] === ']') {\n depth--;\n if (depth === 0) {\n end = i;\n break;\n }\n }\n }\n if (end < 0) return undefined;\n const inner = after.slice(1, end);\n return inner\n .split(',')\n .map((s) => s.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter((s) => s.length > 0);\n}\n"],"mappings":"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","names":[]}
|
package/dist/commit-hook.js
CHANGED
|
@@ -25,7 +25,9 @@ function makeCommitHook(opts) {
|
|
|
25
25
|
const relPath = relativeTo(cwd, filePath);
|
|
26
26
|
const addRes = spawn("git", ["add", relPath], { cwd, encoding: "utf8" });
|
|
27
27
|
if (addRes.status !== 0) {
|
|
28
|
-
throw new Error(
|
|
28
|
+
throw new Error(
|
|
29
|
+
`git add failed: ${addRes.stderr || addRes.stdout || `exit ${addRes.status}`}`
|
|
30
|
+
);
|
|
29
31
|
}
|
|
30
32
|
const message = renderCommitMessage({ scope, task, identity, result });
|
|
31
33
|
const commitArgs = ["commit", "-m", message];
|
|
@@ -34,7 +36,9 @@ function makeCommitHook(opts) {
|
|
|
34
36
|
}
|
|
35
37
|
const commitRes = spawn("git", commitArgs, { cwd, encoding: "utf8" });
|
|
36
38
|
if (commitRes.status !== 0) {
|
|
37
|
-
throw new Error(
|
|
39
|
+
throw new Error(
|
|
40
|
+
`git commit failed: ${commitRes.stderr || commitRes.stdout || `exit ${commitRes.status}`}`
|
|
41
|
+
);
|
|
38
42
|
}
|
|
39
43
|
const hashRes = spawn("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" });
|
|
40
44
|
const commitHash = hashRes.status === 0 ? hashRes.stdout.trim() : void 0;
|
package/dist/commit-hook.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commit-hook.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport type { AgentIdentity, BoardTask, ExecutionResult } from './types.js';\n\nexport interface CommitHookOptions {\n outputDir: string;\n workingDir?: string;\n authorName?: string;\n authorEmail?: string;\n scope?: string;\n spawn?: typeof spawnSync;\n now?: () => Date;\n}\n\nexport interface CommitHookResult {\n filePath: string;\n commitHash?: string;\n staged: string[];\n message: string;\n}\n\nconst SAFE_HANDLE = /^[a-z0-9_-]{1,64}$/i;\n\nexport function makeCommitHook(opts: CommitHookOptions) {\n if (!opts.outputDir || opts.outputDir.trim().length === 0) {\n throw new Error('CommitHookOptions.outputDir is required');\n }\n const spawn = opts.spawn ?? spawnSync;\n const cwd = opts.workingDir ?? process.cwd();\n const outputDir = resolve(cwd, opts.outputDir);\n const now = opts.now ?? (() => new Date());\n const scope = opts.scope ?? 'agent';\n\n return async (result: ExecutionResult
|
|
1
|
+
{"version":3,"sources":["../src/commit-hook.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport type { AgentIdentity, BoardTask, ExecutionResult } from './types.js';\n\nexport interface CommitHookOptions {\n outputDir: string;\n workingDir?: string;\n authorName?: string;\n authorEmail?: string;\n scope?: string;\n spawn?: typeof spawnSync;\n now?: () => Date;\n}\n\nexport interface CommitHookResult {\n filePath: string;\n commitHash?: string;\n staged: string[];\n message: string;\n}\n\nconst SAFE_HANDLE = /^[a-z0-9_-]{1,64}$/i;\n\nexport function makeCommitHook(opts: CommitHookOptions) {\n if (!opts.outputDir || opts.outputDir.trim().length === 0) {\n throw new Error('CommitHookOptions.outputDir is required');\n }\n const spawn = opts.spawn ?? spawnSync;\n const cwd = opts.workingDir ?? process.cwd();\n const outputDir = resolve(cwd, opts.outputDir);\n const now = opts.now ?? (() => new Date());\n const scope = opts.scope ?? 'agent';\n\n return async (\n result: ExecutionResult,\n task: BoardTask,\n identity: AgentIdentity\n ): Promise<CommitHookResult> => {\n if (!SAFE_HANDLE.test(identity.handle)) {\n throw new Error(`Refusing to commit: handle \"${identity.handle}\" must match ${SAFE_HANDLE}`);\n }\n const date = now().toISOString().slice(0, 10);\n const safeTaskId = task.id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80);\n const fileName = `${date}_${safeTaskId}_${identity.handle}.md`;\n const filePath = join(outputDir, fileName);\n\n mkdirSync(dirname(filePath), { recursive: true });\n writeFileSync(filePath, renderMemo(result, task, identity, date), 'utf8');\n\n const relPath = relativeTo(cwd, filePath);\n const addRes = spawn('git', ['add', relPath], { cwd, encoding: 'utf8' });\n if (addRes.status !== 0) {\n throw new Error(\n `git add failed: ${addRes.stderr || addRes.stdout || `exit ${addRes.status}`}`\n );\n }\n\n const message = renderCommitMessage({ scope, task, identity, result });\n const commitArgs = ['commit', '-m', message];\n if (opts.authorName && opts.authorEmail) {\n commitArgs.push('--author', `${opts.authorName} <${opts.authorEmail}>`);\n }\n const commitRes = spawn('git', commitArgs, { cwd, encoding: 'utf8' });\n if (commitRes.status !== 0) {\n throw new Error(\n `git commit failed: ${commitRes.stderr || commitRes.stdout || `exit ${commitRes.status}`}`\n );\n }\n\n const hashRes = spawn('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8' });\n const commitHash = hashRes.status === 0 ? hashRes.stdout.trim() : undefined;\n\n return { filePath, commitHash, staged: [relPath], message };\n };\n}\n\nfunction renderMemo(\n result: ExecutionResult,\n task: BoardTask,\n identity: AgentIdentity,\n date: string\n): string {\n return [\n '---',\n `title: \"${task.title.replace(/\"/g, \"'\")}\"`,\n `task_id: ${task.id}`,\n `agent: ${identity.handle}`,\n `surface: ${identity.surface}`,\n `provider: ${identity.llmProvider}`,\n `model: ${identity.llmModel}`,\n `wallet: ${identity.wallet}`,\n `date: ${date}`,\n `tokens: ${result.usage.totalTokens}`,\n `cost_usd: ${result.costUsd.toFixed(4)}`,\n `duration_ms: ${result.durationMs}`,\n `tags: [${(task.tags ?? []).map((t) => JSON.stringify(t)).join(', ')}]`,\n '---',\n '',\n `# ${task.title}`,\n '',\n '## Task description',\n '',\n task.description ?? '(no description)',\n '',\n '## Agent response',\n '',\n result.responseText.trim(),\n '',\n ].join('\\n');\n}\n\nconst SUBJECT_MAX = 72;\n\nfunction renderCommitMessage(opts: {\n scope: string;\n task: BoardTask;\n identity: AgentIdentity;\n result: ExecutionResult;\n}): string {\n const suffix = ` [agent:${opts.identity.handle}]`;\n const prefix = `${opts.scope}: `;\n const titleBudget = Math.max(8, SUBJECT_MAX - prefix.length - suffix.length);\n const subject = `${prefix}${truncate(opts.task.title, titleBudget)}${suffix}`;\n const body = [\n '',\n `task: ${opts.task.id}`,\n `agent: ${opts.identity.handle} (${opts.identity.llmProvider}/${opts.identity.llmModel})`,\n `wallet: ${opts.identity.wallet}`,\n `cost: $${opts.result.costUsd.toFixed(4)} / ${opts.result.usage.totalTokens} tok / ${opts.result.durationMs}ms`,\n ].join('\\n');\n return `${subject}\\n${body}\\n`;\n}\n\nfunction truncate(s: string, max: number): string {\n return s.length <= max ? s : `${s.slice(0, max - 1)}…`;\n}\n\nfunction relativeTo(base: string, target: string): string {\n const b = base.replace(/\\\\/g, '/');\n const t = target.replace(/\\\\/g, '/');\n if (t.startsWith(b + '/')) return t.slice(b.length + 1);\n if (t === b) return '.';\n return t;\n}\n"],"mappings":"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","names":[]}
|
package/dist/cost-guard.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
3
3
|
import { dirname } from "path";
|
|
4
4
|
var ANTHROPIC_PRICING_USD_PER_MTOK = {
|
|
5
|
+
"claude-opus-4-8": { input: 10, output: 50 },
|
|
6
|
+
// 3× cheaper than 4.7 on total cost; A-020 2026-06-08
|
|
5
7
|
"claude-opus-4-7": { input: 5, output: 25 },
|
|
6
8
|
"claude-opus-4-6": { input: 5, output: 25 },
|
|
7
9
|
"claude-sonnet-4-6": { input: 3, output: 15 },
|
package/dist/cost-guard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cost-guard.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { TokenUsage } from '@holoscript/llm-provider';\nimport type { CostState, ModelPricer } from './types.js';\n\nexport const ANTHROPIC_PRICING_USD_PER_MTOK: Record<string, { input: number; output: number }> = {\n 'claude-opus-4-7': { input: 5, output: 25 },\n 'claude-opus-4-6': { input: 5, output: 25 },\n 'claude-sonnet-4-6': { input: 3, output: 15 },\n 'claude-haiku-4-5-20251001': { input: 1, output: 5 },\n 'claude-haiku-4-5': { input: 1, output: 5 },\n};\n\nexport function defaultAnthropicPricer(model: string, usage: TokenUsage): number {\n const price = ANTHROPIC_PRICING_USD_PER_MTOK[model];\n if (!price) {\n throw new Error(\n `No pricing configured for model \"${model}\" — add to ANTHROPIC_PRICING_USD_PER_MTOK or pass a custom pricer`\n );\n }\n return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1_000_000;\n}\n\n/**\n * Pricer for local-llm providers (vLLM-on-GPU). The compute cost is the\n * Vast.ai (or other GPU) hourly rental, NOT per-token. From the agent's\n * perspective each LLM call has $0 marginal cost — the budget guard for\n * local-llm should track tick count or wall-clock time, not tokens.\n *\n * Returns 0 unconditionally. Token counts are still recorded in CostState\n * so usage analytics work, but cost-guard never trips on token spend.\n */\nexport function defaultLocalLlmPricer(_model: string, _usage: TokenUsage): number {\n return 0;\n}\n\n// xAI / Grok pricing — populated by /research task task_1778109552044_qed8.\n// Empty until verified pricing lands. defaultXAIPricer throws on missing\n// model with a helpful pointer (matches defaultAnthropicPricer behavior).\n// Never paste training-era pricing here — F.014 / W.GOLD.341.\nexport const XAI_PRICING_USD_PER_MTOK: Record<string, { input: number; output: number }> = {};\n\nexport function defaultXAIPricer(model: string, usage: TokenUsage): number {\n const price = XAI_PRICING_USD_PER_MTOK[model];\n if (!price) {\n throw new Error(\n `No xAI pricing configured for model \"${model}\" — populate XAI_PRICING_USD_PER_MTOK ` +\n `(see /research task_1778109552044_qed8 in docs/LLM_CAPABILITIES.md) or pass a custom pricer`\n );\n }\n return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1_000_000;\n}\n\n// OpenRouter pricing is per-model and varies by upstream — populated lazily.\n// Empty until verified pricing lands.\nexport const OPENROUTER_PRICING_USD_PER_MTOK: Record<string, { input: number; output: number }> =\n {};\n\nexport function defaultOpenRouterPricer(model: string, usage: TokenUsage): number {\n const price = OPENROUTER_PRICING_USD_PER_MTOK[model];\n if (!price) {\n throw new Error(\n `No OpenRouter pricing configured for model \"${model}\" — populate OPENROUTER_PRICING_USD_PER_MTOK ` +\n `or pass a custom pricer`\n );\n }\n return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1_000_000;\n}\n\n/**\n * Provider-aware default pricer dispatch. Picks the right pricer by\n * provider so the holoscript-agent runtime works for both Anthropic\n * (per-token billing) and local-llm (compute already paid via GPU\n * rental) without a custom pricer at every call site.\n *\n * Refs: 2026-04-26 mw02 boot loop — local-llm workers tick-erroring with\n * \"No pricing configured for model 'Qwen/Qwen2.5-0.5B-Instruct'\" because\n * defaultAnthropicPricer was wired in for ALL providers regardless of\n * which LLM the agent uses.\n *\n * Known gap (separate task): non-Anthropic non-local providers (openai,\n * gemini) still fall through to defaultAnthropicPricer here. xai +\n * openrouter were added 2026-05-06 with explicit dispatch + empty\n * pricing dicts (Lane A — see docs/LLM_CAPABILITIES.md).\n */\nexport function defaultPricerForProvider(\n provider: 'anthropic' | 'local-llm' | 'openai' | 'xai' | 'openrouter' | string\n): ModelPricer {\n if (provider === 'local-llm' || provider === 'mock') return defaultLocalLlmPricer;\n if (provider === 'xai') return defaultXAIPricer;\n if (provider === 'openrouter') return defaultOpenRouterPricer;\n return defaultAnthropicPricer;\n}\n\nexport class CostGuard {\n private state: CostState;\n private readonly statePath: string;\n private readonly dailyBudgetUsd: number;\n private readonly pricer: ModelPricer;\n\n constructor(opts: { statePath: string; dailyBudgetUsd: number; pricer?: ModelPricer }) {\n this.statePath = opts.statePath;\n this.dailyBudgetUsd = opts.dailyBudgetUsd;\n this.pricer = opts.pricer ?? defaultAnthropicPricer;\n this.state = this.loadOrInit();\n }\n\n recordUsage(\n model: string,\n usage: TokenUsage\n ): { costUsd: number; spentUsd: number; remainingUsd: number } {\n this.rolloverIfNewDay();\n const costUsd = this.pricer(model, usage);\n this.state.spentUsd += costUsd;\n this.state.promptTokens += usage.promptTokens;\n this.state.completionTokens += usage.completionTokens;\n this.state.callCount += 1;\n this.persist();\n return {\n costUsd,\n spentUsd: this.state.spentUsd,\n remainingUsd: Math.max(0, this.dailyBudgetUsd - this.state.spentUsd),\n };\n }\n\n isOverBudget(): boolean {\n if (this.dailyBudgetUsd === 0) return false;\n this.rolloverIfNewDay();\n return this.state.spentUsd >= this.dailyBudgetUsd;\n }\n\n getRemainingUsd(): number {\n if (this.dailyBudgetUsd === 0) return Number.POSITIVE_INFINITY;\n this.rolloverIfNewDay();\n return Math.max(0, this.dailyBudgetUsd - this.state.spentUsd);\n }\n\n getState(): Readonly<CostState> {\n this.rolloverIfNewDay();\n return { ...this.state };\n }\n\n private rolloverIfNewDay(): void {\n const today = todayUtc();\n if (this.state.date !== today) {\n this.state = { date: today, spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };\n this.persist();\n }\n }\n\n private loadOrInit(): CostState {\n if (existsSync(this.statePath)) {\n const raw = readFileSync(this.statePath, 'utf8');\n const parsed = JSON.parse(raw) as CostState;\n if (parsed.date === todayUtc()) return parsed;\n }\n return { date: todayUtc(), spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };\n }\n\n private persist(): void {\n mkdirSync(dirname(this.statePath), { recursive: true });\n writeFileSync(this.statePath, JSON.stringify(this.state, null, 2), 'utf8');\n }\n}\n\nfunction todayUtc(): string {\n return new Date().toISOString().slice(0, 10);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cost-guard.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { TokenUsage } from '@holoscript/llm-provider';\nimport type { CostState, ModelPricer } from './types.js';\n\nexport const ANTHROPIC_PRICING_USD_PER_MTOK: Record<string, { input: number; output: number }> = {\n 'claude-opus-4-8': { input: 10, output: 50 }, // 3× cheaper than 4.7 on total cost; A-020 2026-06-08\n 'claude-opus-4-7': { input: 5, output: 25 },\n 'claude-opus-4-6': { input: 5, output: 25 },\n 'claude-sonnet-4-6': { input: 3, output: 15 },\n 'claude-haiku-4-5-20251001': { input: 1, output: 5 },\n 'claude-haiku-4-5': { input: 1, output: 5 },\n};\n\nexport function defaultAnthropicPricer(model: string, usage: TokenUsage): number {\n const price = ANTHROPIC_PRICING_USD_PER_MTOK[model];\n if (!price) {\n throw new Error(\n `No pricing configured for model \"${model}\" — add to ANTHROPIC_PRICING_USD_PER_MTOK or pass a custom pricer`\n );\n }\n return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1_000_000;\n}\n\n/**\n * Pricer for local-llm providers (vLLM-on-GPU). The compute cost is the\n * Vast.ai (or other GPU) hourly rental, NOT per-token. From the agent's\n * perspective each LLM call has $0 marginal cost — the budget guard for\n * local-llm should track tick count or wall-clock time, not tokens.\n *\n * Returns 0 unconditionally. Token counts are still recorded in CostState\n * so usage analytics work, but cost-guard never trips on token spend.\n */\nexport function defaultLocalLlmPricer(_model: string, _usage: TokenUsage): number {\n return 0;\n}\n\n// xAI / Grok pricing — populated by /research task task_1778109552044_qed8.\n// Empty until verified pricing lands. defaultXAIPricer throws on missing\n// model with a helpful pointer (matches defaultAnthropicPricer behavior).\n// Never paste training-era pricing here — F.014 / W.GOLD.341.\nexport const XAI_PRICING_USD_PER_MTOK: Record<string, { input: number; output: number }> = {};\n\nexport function defaultXAIPricer(model: string, usage: TokenUsage): number {\n const price = XAI_PRICING_USD_PER_MTOK[model];\n if (!price) {\n throw new Error(\n `No xAI pricing configured for model \"${model}\" — populate XAI_PRICING_USD_PER_MTOK ` +\n `(see /research task_1778109552044_qed8 in docs/LLM_CAPABILITIES.md) or pass a custom pricer`\n );\n }\n return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1_000_000;\n}\n\n// OpenRouter pricing is per-model and varies by upstream — populated lazily.\n// Empty until verified pricing lands.\nexport const OPENROUTER_PRICING_USD_PER_MTOK: Record<string, { input: number; output: number }> =\n {};\n\nexport function defaultOpenRouterPricer(model: string, usage: TokenUsage): number {\n const price = OPENROUTER_PRICING_USD_PER_MTOK[model];\n if (!price) {\n throw new Error(\n `No OpenRouter pricing configured for model \"${model}\" — populate OPENROUTER_PRICING_USD_PER_MTOK ` +\n `or pass a custom pricer`\n );\n }\n return (usage.promptTokens * price.input + usage.completionTokens * price.output) / 1_000_000;\n}\n\n/**\n * Provider-aware default pricer dispatch. Picks the right pricer by\n * provider so the holoscript-agent runtime works for both Anthropic\n * (per-token billing) and local-llm (compute already paid via GPU\n * rental) without a custom pricer at every call site.\n *\n * Refs: 2026-04-26 mw02 boot loop — local-llm workers tick-erroring with\n * \"No pricing configured for model 'Qwen/Qwen2.5-0.5B-Instruct'\" because\n * defaultAnthropicPricer was wired in for ALL providers regardless of\n * which LLM the agent uses.\n *\n * Known gap (separate task): non-Anthropic non-local providers (openai,\n * gemini) still fall through to defaultAnthropicPricer here. xai +\n * openrouter were added 2026-05-06 with explicit dispatch + empty\n * pricing dicts (Lane A — see docs/LLM_CAPABILITIES.md).\n */\nexport function defaultPricerForProvider(\n provider: 'anthropic' | 'local-llm' | 'openai' | 'xai' | 'openrouter' | string\n): ModelPricer {\n if (provider === 'local-llm' || provider === 'mock') return defaultLocalLlmPricer;\n if (provider === 'xai') return defaultXAIPricer;\n if (provider === 'openrouter') return defaultOpenRouterPricer;\n return defaultAnthropicPricer;\n}\n\nexport class CostGuard {\n private state: CostState;\n private readonly statePath: string;\n private readonly dailyBudgetUsd: number;\n private readonly pricer: ModelPricer;\n\n constructor(opts: { statePath: string; dailyBudgetUsd: number; pricer?: ModelPricer }) {\n this.statePath = opts.statePath;\n this.dailyBudgetUsd = opts.dailyBudgetUsd;\n this.pricer = opts.pricer ?? defaultAnthropicPricer;\n this.state = this.loadOrInit();\n }\n\n recordUsage(\n model: string,\n usage: TokenUsage\n ): { costUsd: number; spentUsd: number; remainingUsd: number } {\n this.rolloverIfNewDay();\n const costUsd = this.pricer(model, usage);\n this.state.spentUsd += costUsd;\n this.state.promptTokens += usage.promptTokens;\n this.state.completionTokens += usage.completionTokens;\n this.state.callCount += 1;\n this.persist();\n return {\n costUsd,\n spentUsd: this.state.spentUsd,\n remainingUsd: Math.max(0, this.dailyBudgetUsd - this.state.spentUsd),\n };\n }\n\n isOverBudget(): boolean {\n if (this.dailyBudgetUsd === 0) return false;\n this.rolloverIfNewDay();\n return this.state.spentUsd >= this.dailyBudgetUsd;\n }\n\n getRemainingUsd(): number {\n if (this.dailyBudgetUsd === 0) return Number.POSITIVE_INFINITY;\n this.rolloverIfNewDay();\n return Math.max(0, this.dailyBudgetUsd - this.state.spentUsd);\n }\n\n getState(): Readonly<CostState> {\n this.rolloverIfNewDay();\n return { ...this.state };\n }\n\n private rolloverIfNewDay(): void {\n const today = todayUtc();\n if (this.state.date !== today) {\n this.state = { date: today, spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };\n this.persist();\n }\n }\n\n private loadOrInit(): CostState {\n if (existsSync(this.statePath)) {\n const raw = readFileSync(this.statePath, 'utf8');\n const parsed = JSON.parse(raw) as CostState;\n if (parsed.date === todayUtc()) return parsed;\n }\n return { date: todayUtc(), spentUsd: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };\n }\n\n private persist(): void {\n mkdirSync(dirname(this.statePath), { recursive: true });\n writeFileSync(this.statePath, JSON.stringify(this.state, null, 2), 'utf8');\n }\n}\n\nfunction todayUtc(): string {\n return new Date().toISOString().slice(0, 10);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -27,11 +27,15 @@ interface CaelAuditRecord {
|
|
|
27
27
|
trust_epoch?: 'post-w107';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Wraps a request body in a signed envelope for strict-mode endpoints (e.g. /team/:id/join). */
|
|
31
|
+
type RequestSigner = (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
30
32
|
interface HolomeshClientOptions {
|
|
31
33
|
apiBase: string;
|
|
32
34
|
bearer: string;
|
|
33
35
|
teamId: string;
|
|
34
36
|
fetchImpl?: typeof fetch;
|
|
37
|
+
/** EIP-191 signing function. When present, used on strict endpoints like joinTeam(). */
|
|
38
|
+
signer?: RequestSigner;
|
|
35
39
|
}
|
|
36
40
|
interface TeamMessage {
|
|
37
41
|
id: string;
|
|
@@ -46,7 +50,10 @@ declare class HolomeshClient {
|
|
|
46
50
|
private readonly bearer;
|
|
47
51
|
private readonly teamId;
|
|
48
52
|
private readonly fetchImpl;
|
|
53
|
+
private readonly signer?;
|
|
49
54
|
constructor(opts: HolomeshClientOptions);
|
|
55
|
+
/** Wrap body in a signed envelope when a signer is available (strict-mode endpoints). */
|
|
56
|
+
private signBody;
|
|
50
57
|
heartbeat(payload: {
|
|
51
58
|
agentName: string;
|
|
52
59
|
surface: string;
|
|
@@ -109,4 +116,4 @@ declare class HolomeshClient {
|
|
|
109
116
|
declare function deriveSurface(seatName: string | undefined): string;
|
|
110
117
|
declare function pickClaimableTask(tasks: BoardTask[], brainCapabilityTags: string[]): BoardTask | undefined;
|
|
111
118
|
|
|
112
|
-
export { HolomeshClient, type HolomeshClientOptions, type TeamMessage, deriveSurface, pickClaimableTask };
|
|
119
|
+
export { HolomeshClient, type HolomeshClientOptions, type RequestSigner, type TeamMessage, deriveSurface, pickClaimableTask };
|
package/dist/holomesh-client.js
CHANGED
|
@@ -5,9 +5,14 @@ var HolomeshClient = class {
|
|
|
5
5
|
this.bearer = opts.bearer;
|
|
6
6
|
this.teamId = opts.teamId;
|
|
7
7
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
8
|
+
this.signer = opts.signer;
|
|
9
|
+
}
|
|
10
|
+
/** Wrap body in a signed envelope when a signer is available (strict-mode endpoints). */
|
|
11
|
+
async signBody(body) {
|
|
12
|
+
return this.signer ? await this.signer(body) : body;
|
|
8
13
|
}
|
|
9
14
|
async heartbeat(payload) {
|
|
10
|
-
await this.req("POST", `/team/${this.teamId}/presence`, payload);
|
|
15
|
+
await this.req("POST", `/team/${this.teamId}/presence`, await this.signBody(payload));
|
|
11
16
|
}
|
|
12
17
|
async getOpenTasks() {
|
|
13
18
|
const data = await this.req(
|
|
@@ -17,28 +22,33 @@ var HolomeshClient = class {
|
|
|
17
22
|
return data.tasks ?? data.open ?? [];
|
|
18
23
|
}
|
|
19
24
|
async claim(taskId) {
|
|
20
|
-
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, { action: "claim" });
|
|
25
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "claim" }));
|
|
21
26
|
}
|
|
22
27
|
async joinTeam() {
|
|
23
28
|
return this.req(
|
|
24
29
|
"POST",
|
|
25
30
|
`/team/${this.teamId}/join`,
|
|
26
|
-
{}
|
|
31
|
+
await this.signBody({})
|
|
27
32
|
);
|
|
28
33
|
}
|
|
29
34
|
async sendMessageOnTask(taskId, body) {
|
|
30
|
-
await this.req("POST", `/team/${this.teamId}/message`, {
|
|
35
|
+
await this.req("POST", `/team/${this.teamId}/message`, await this.signBody({
|
|
31
36
|
to: "team",
|
|
32
37
|
subject: `task:${taskId}`,
|
|
33
38
|
content: body
|
|
34
|
-
});
|
|
39
|
+
}));
|
|
35
40
|
}
|
|
36
41
|
async markDone(taskId, summary, commitHash) {
|
|
37
|
-
await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
42
|
+
await this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({
|
|
38
43
|
action: "done",
|
|
39
44
|
summary,
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
// verification_evidence required by server before task can be closed.
|
|
46
|
+
verification_evidence: summary,
|
|
47
|
+
// Exclude commitHash when undefined — JSON.stringify drops undefined but
|
|
48
|
+
// canonicalizeSigning preserves it as the literal string "undefined",
|
|
49
|
+
// causing a signature-mismatch vs what the server sees after JSON.parse.
|
|
50
|
+
...commitHash !== void 0 ? { commitHash } : {}
|
|
51
|
+
}));
|
|
42
52
|
}
|
|
43
53
|
// POST CAEL audit records for this agent. Server validator at
|
|
44
54
|
// packages/mcp-server/src/holomesh/routes/core-routes.ts:472-533 requires
|
|
@@ -72,39 +82,28 @@ var HolomeshClient = class {
|
|
|
72
82
|
}
|
|
73
83
|
/** Post a message to the team feed. */
|
|
74
84
|
async sendTeamMessage(content, messageType = "text") {
|
|
75
|
-
await this.req("POST", `/team/${this.teamId}/message`, {
|
|
76
|
-
content,
|
|
77
|
-
type: messageType
|
|
78
|
-
});
|
|
85
|
+
await this.req("POST", `/team/${this.teamId}/message`, await this.signBody({ content, type: messageType }));
|
|
79
86
|
}
|
|
80
87
|
// ── Owner-op API wrappers (E4) ─────────────────────────────────────────────
|
|
81
88
|
/** Switch team mode. Requires owner or founder role. */
|
|
82
89
|
async setTeamMode(mode, reason) {
|
|
83
|
-
return this.req("POST", `/team/${this.teamId}/mode`, { mode, reason });
|
|
90
|
+
return this.req("POST", `/team/${this.teamId}/mode`, await this.signBody({ mode, reason }));
|
|
84
91
|
}
|
|
85
92
|
/** Update room preferences. Requires config:write permission. */
|
|
86
93
|
async patchRoomPrefs(prefs) {
|
|
87
|
-
return this.req("PATCH", `/team/${this.teamId}/room`, prefs);
|
|
94
|
+
return this.req("PATCH", `/team/${this.teamId}/room`, await this.signBody(prefs));
|
|
88
95
|
}
|
|
89
96
|
/** Update a board task. */
|
|
90
97
|
async updateTask(taskId, updates) {
|
|
91
|
-
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
92
|
-
action: "update",
|
|
93
|
-
...updates
|
|
94
|
-
});
|
|
98
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "update", ...updates }));
|
|
95
99
|
}
|
|
96
100
|
/** Delete a board task. */
|
|
97
101
|
async deleteTask(taskId) {
|
|
98
|
-
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
99
|
-
action: "delete"
|
|
100
|
-
});
|
|
102
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "delete" }));
|
|
101
103
|
}
|
|
102
104
|
/** Delegate a board task to another agent. */
|
|
103
105
|
async delegateTask(taskId, toAgentId) {
|
|
104
|
-
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, {
|
|
105
|
-
action: "delegate",
|
|
106
|
-
toAgentId
|
|
107
|
-
});
|
|
106
|
+
return this.req("PATCH", `/team/${this.teamId}/board/${taskId}`, await this.signBody({ action: "delegate", toAgentId }));
|
|
108
107
|
}
|
|
109
108
|
async req(method, path, body) {
|
|
110
109
|
const url = `${this.apiBase}${path}`;
|