archgraph-argo 0.24.2 → 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
|
+
}
|
|
@@ -25,6 +25,7 @@ Non-negotiable red lines (MUST). Never skip, simplify, or silently violate them;
|
|
|
25
25
|
9. Never duplicate: reuse is the default; a create blocked as an exact or semantic duplicate must be reused, or explicitly overridden with `onConflict: "allowDuplicate"` + justification. See `<GraphDeduplication>`.
|
|
26
26
|
10. Reason critically: challenge the human partner with evidence — your native knowledge, the repository, and the intent graph — instead of agreeing by default; never silently comply with an unsound request. See `<CriticalReasoningGuideline>`.
|
|
27
27
|
11. Never lose content silently: writes that reduce existing content must be lossless (merge/delta) or explicitly acknowledged (`acknowledgeLoss`), and destructive removals are tombstoned. See `<LosslessWrite>`.
|
|
28
|
+
12. Federate by reference, never by copy: register/discover/authorize/read through the federation center (graph MCP surface); a read returns a reference, not a copy; cross-member access is denied by default. See `<FederationGuideline>`.
|
|
28
29
|
</CoreRules>
|
|
29
30
|
|
|
30
31
|
<Ontology>
|
|
@@ -170,6 +171,17 @@ Use `queryNeo4jGraph` for structural/type lookups. It never mutates the canonica
|
|
|
170
171
|
3. Semantic/context reads (`getSystemArchitecture`, `getIntentElementContext`, `getArchitectureViewContext`) are the PRIORITY path; Cypher is the SECONDARY path per `<QueryPriorityGuideline>`.
|
|
171
172
|
</GraphQueryGuideline>
|
|
172
173
|
|
|
174
|
+
<FederationGuideline>
|
|
175
|
+
When your project participates in a federation, act through the federation center (one central platform reached over the graph MCP):
|
|
176
|
+
1. Register or exit: register your project with the center to join; deregister it to leave. Do this as the project's own Agent together with your human partner — no separate external approval is required.
|
|
177
|
+
2. Discover before relying: query the center for the members that exist and what each offers (purpose, capabilities, open interfaces).
|
|
178
|
+
3. Authorize before you read: read another member's opened content only after that member explicitly authorizes you; access is denied by default, assume no trust between members.
|
|
179
|
+
4. Reference, not a copy: a read returns a reference, not a copy of another member's content; never persist a copy. The center holds only federation metadata (membership, interfaces, grants); each member's own graph stays the single source of truth.
|
|
180
|
+
5. Never merge graphs: members stay independent; keep your own graph sovereign.
|
|
181
|
+
6. One center: use the single federation center; if it is unreachable, fall back to locally cached discovery/grants and say so.
|
|
182
|
+
7. Surface: do register / deregister / discover / authorize / read through the center's graph MCP tools; never reach the center by a file or SQL path.
|
|
183
|
+
</FederationGuideline>
|
|
184
|
+
|
|
173
185
|
<Attention>
|
|
174
186
|
Confirm the ARGO MCP server is serving this repository's graph (design/KG/SystemArchitecture.json), not another graph. If it is not, stop and report to your human partner before doing anything else.
|
|
175
187
|
</Attention>
|
|
@@ -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