archgraph-argo 0.24.3 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ // Argo agent-cost collector plugin for opencode.
2
+ //
3
+ // The framework records ONE consolidated agent-cost log per workspace at
4
+ // <workspace>/.argo/temp/agent-cost-log.ndjson, so the user fetches a single
5
+ // file instead of stitching logs from several places. This plugin is the ONLY
6
+ // collector: it sees EVERY tool call the agent makes — MCP interface calls
7
+ // (e.g. getSystemArchitecture), graph writes (e.g. applySystemArchitectureMutation)
8
+ // and repository calls (read / grep / glob) alike — plus assistant token/cost
9
+ // usage, which an MCP-side hook could never observe completely.
10
+ //
11
+ // Recording only observes; it never changes retrieval. Disable with
12
+ // ARGO_COST_PROFILER=0.
13
+
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ let log = null;
18
+ try {
19
+ log = require('../scripts/graph-rag/agentCostLog.js');
20
+ } catch {
21
+ // The shared log module ships next to the plugins under ~/.argo; if it is
22
+ // missing this plugin is a no-op so it can never break a session.
23
+ log = null;
24
+ }
25
+
26
+ export default async function argoCostCollector(input) {
27
+ if (!log) return {};
28
+ const workspaceRoot = (input && (input.directory || input.worktree)) || process.cwd();
29
+ const hooks = log.createHostCollectorHooks(workspaceRoot);
30
+ return {
31
+ "tool.execute.before": async (i) => { try { hooks.before(i); } catch { /* best-effort */ } },
32
+ "tool.execute.after": async (i, o) => { try { hooks.after(i, o); } catch { /* best-effort */ } },
33
+ event: async (payload) => { try { hooks.event(payload); } catch { /* best-effort */ } },
34
+ };
35
+ }
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ // Agent cost LOG (framework, zero-config, background) — the single consolidated
4
+ // store for everything needed to reason about an agent's retrieval cost.
5
+ //
6
+ // WHY ONE FILE: in a real project the agent's scope is the whole repository (an
7
+ // unbounded content source) plus the curated intent graph (KG, only a semantic
8
+ // index / routing layer). Cost is dominated by the agent's round-trips BETWEEN
9
+ // the two backends (graph: getSystemArchitecture / getIntentElementContext /
10
+ // getArchitectureViewContext / queryNeo4jGraph / memory_search; repository:
11
+ // read / grep / glob), not by the size of either backend. To optimise without
12
+ // hurting recall we must measure that — in ONE place the user fetches once:
13
+ //
14
+ // <workspace>/.argo/temp/agent-cost-log.ndjson
15
+ //
16
+ // WHO WRITES: the OpenCode plugin argo/plugins/argo-cost-collector.js. Every
17
+ // tool call the agent makes — MCP interface calls, graph writes, and repository
18
+ // calls alike — is a host tool, so the plugin records ALL of them, plus the
19
+ // assistant's token/cost usage. There is deliberately NO MCP-side instrumentation
20
+ // (a record without the host side would be incomplete and thus misleading).
21
+ //
22
+ // Posture (mirrors mcpCrashDiagnostics): best-effort, never throws, never logs
23
+ // secret values, and NEVER changes retrieval (it observes results only).
24
+
25
+ const fs = require('node:fs');
26
+ const path = require('node:path');
27
+
28
+ const LOG_FILE_NAME = 'agent-cost-log.ndjson';
29
+ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
30
+
31
+ const GRAPH_TOOLS = ['getSystemArchitecture', 'getIntentElementContext', 'getArchitectureViewContext', 'queryNeo4jGraph', 'memory_search'];
32
+ const GRAPH_WRITE_TOOLS = [
33
+ 'previewSystemArchitectureMutation', 'applySystemArchitectureMutation',
34
+ 'addArchitectureElement', 'updateArchitectureElement', 'removeArchitectureElement',
35
+ 'addArchitectureRelationship', 'updateArchitectureRelationship', 'removeArchitectureRelationship',
36
+ 'addArchitectureView', 'updateArchitectureView', 'removeArchitectureView',
37
+ ];
38
+ const VALIDATOR_TOOLS = ['validateSystemArchitecture', 'runArchitectureTests', 'initializeWorkspace'];
39
+ const REPO_TOOLS = ['read', 'grep', 'glob', 'list', 'bash', 'webfetch', 'edit', 'write'];
40
+
41
+ function enabled() {
42
+ return process.env.ARGO_COST_PROFILER !== '0';
43
+ }
44
+
45
+ function maxBytes() {
46
+ const n = Number(process.env.ARGO_COST_TRACE_MAX_BYTES);
47
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_BYTES;
48
+ }
49
+
50
+ function tempDir(workspaceRoot) {
51
+ return path.join(workspaceRoot, '.argo', 'temp');
52
+ }
53
+
54
+ function logFilePath(workspaceRoot) {
55
+ return path.join(tempDir(workspaceRoot), LOG_FILE_NAME);
56
+ }
57
+
58
+ // Pure, total classification. Host tool names may be prefixed (e.g.
59
+ // mcp__argo__getSystemArchitecture), so match by substring.
60
+ function classifyTool(tool) {
61
+ const name = String(tool || '');
62
+ const hit = (list) => list.some(t => name.includes(t));
63
+ if (hit(GRAPH_WRITE_TOOLS)) return { backend: 'graph', kind: 'write' };
64
+ if (hit(GRAPH_TOOLS)) return { backend: 'graph', kind: 'read' };
65
+ if (hit(VALIDATOR_TOOLS)) return { backend: 'framework', kind: 'framework' };
66
+ if (hit(REPO_TOOLS)) return { backend: 'repo', kind: 'read' };
67
+ return { backend: 'other', kind: 'other' };
68
+ }
69
+
70
+ // Deterministic coarse token estimate: CJK ≈ 1 token/char, else ≈ 1/4 chars.
71
+ function estimateTokens(text) {
72
+ if (!text) return 0;
73
+ const s = String(text);
74
+ const cjk = (s.match(/[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g) || []).length;
75
+ return cjk + Math.ceil((s.length - cjk) / 4);
76
+ }
77
+
78
+ function rotateIfNeeded(file, limit) {
79
+ try {
80
+ if (fs.statSync(file).size >= limit) fs.renameSync(file, `${file}.1`);
81
+ } catch (_) { /* no existing log */ }
82
+ }
83
+
84
+ // Append one record to the single consolidated log. Best-effort: any failure is
85
+ // swallowed so logging can never affect a tool call.
86
+ function appendRecord(workspaceRoot, record) {
87
+ if (!enabled() || !workspaceRoot || !record || typeof record !== 'object') return;
88
+ try {
89
+ const file = logFilePath(workspaceRoot);
90
+ fs.mkdirSync(path.dirname(file), { recursive: true });
91
+ rotateIfNeeded(file, maxBytes());
92
+ const line = JSON.stringify({ at: new Date().toISOString(), pid: process.pid, ...record }) + '\n';
93
+ fs.appendFileSync(file, line, 'utf8');
94
+ } catch (_) {
95
+ // best-effort only
96
+ }
97
+ }
98
+
99
+ function readLog(workspaceRoot) {
100
+ const file = logFilePath(workspaceRoot);
101
+ let raw = '';
102
+ try { raw = fs.readFileSync(file, 'utf8'); } catch (_) { return { file, records: [] }; }
103
+ const records = [];
104
+ for (const line of raw.split('\n')) {
105
+ const t = line.trim();
106
+ if (!t.startsWith('{')) continue;
107
+ try { records.push(JSON.parse(t)); } catch (_) { /* skip malformed */ }
108
+ }
109
+ return { file, records };
110
+ }
111
+
112
+ function byteLen(value) {
113
+ try {
114
+ const s = JSON.stringify(value);
115
+ return (typeof Buffer !== 'undefined') ? Buffer.byteLength(s) : s.length;
116
+ } catch (_) { return 0; }
117
+ }
118
+
119
+ function keysOf(value) {
120
+ return (value && typeof value === 'object' && !Array.isArray(value)) ? Object.keys(value).sort() : [];
121
+ }
122
+
123
+ // Host-collector hook logic (used by the OpenCode plugin; unit-tested in CJS).
124
+ // It records EVERY host tool call — MCP interface calls, graph writes, and
125
+ // repository calls alike — plus assistant token/cost usage into the single log.
126
+ function createHostCollectorHooks(workspaceRoot) {
127
+ const starts = new Map();
128
+ const seenUsage = new Set();
129
+ return {
130
+ before(input) {
131
+ if (!input || !input.callID) return;
132
+ const args = input.args || {};
133
+ starts.set(input.callID, { t: Date.now(), keys: keysOf(args), bytes: byteLen(args) });
134
+ },
135
+ after(input, output) {
136
+ if (!input) return;
137
+ const s = starts.get(input.callID) || {};
138
+ const cls = classifyTool(input.tool);
139
+ const out = (output && output.output) || '';
140
+ appendRecord(workspaceRoot, {
141
+ source: 'host',
142
+ type: 'tool',
143
+ sessionID: input.sessionID,
144
+ callID: input.callID,
145
+ tool: input.tool,
146
+ backend: cls.backend,
147
+ kind: cls.kind,
148
+ durationMs: s.t ? (Date.now() - s.t) : 0,
149
+ ok: true,
150
+ args: { keys: s.keys || keysOf(input.args), bytes: s.bytes != null ? s.bytes : byteLen(input.args) },
151
+ resultBytes: String(out).length,
152
+ resultTokens: estimateTokens(out),
153
+ });
154
+ starts.delete(input.callID);
155
+ },
156
+ event(payload) {
157
+ const event = payload && payload.event;
158
+ const props = (event && event.properties) || {};
159
+ const info = props.info || (event && event.info) || null;
160
+ if (info && info.role === 'assistant' && info.tokens && info.id && !seenUsage.has(info.id)) {
161
+ seenUsage.add(info.id);
162
+ appendRecord(workspaceRoot, {
163
+ source: 'host',
164
+ type: 'usage',
165
+ sessionID: info.sessionID,
166
+ messageID: info.id,
167
+ tokens: info.tokens,
168
+ cost: info.cost,
169
+ });
170
+ }
171
+ },
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ LOG_FILE_NAME,
177
+ enabled, tempDir, logFilePath, classifyTool, estimateTokens,
178
+ appendRecord, readLog, createHostCollectorHooks,
179
+ };
package/install-argo.ps1 CHANGED
@@ -1109,5 +1109,14 @@ if (Test-Path $wakeupPluginPath) {
1109
1109
  Write-Host "argo-wakeup plugin registered -> $OpenCodeConfigPath"
1110
1110
  }
1111
1111
 
1112
+ # Agent-cost collector: the complete host-side writer of the single consolidated
1113
+ # agent-cost log (<workspace>/.argo/temp/agent-cost-log.ndjson).
1114
+ $costCollectorPath = Join-Path $PluginsRoot 'argo-cost-collector.js'
1115
+ if (Test-Path $costCollectorPath) {
1116
+ Write-Host '==> Registering argo-cost-collector plugin in OpenCode'
1117
+ Register-OpenCodePlugin -ConfigPath $OpenCodeConfigPath -PluginFilePath $costCollectorPath
1118
+ Write-Host "argo-cost-collector plugin registered -> $OpenCodeConfigPath"
1119
+ }
1120
+
1112
1121
  Write-Host ''
1113
1122
  Write-Host 'Argo deployment complete.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.24.3",
3
+ "version": "0.25.0",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {