ai-obs-cli 0.0.0-stage → 0.5.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.
- package/README.md +666 -2
- package/bin/ai-obs.js +8 -0
- package/dist/adapters/claude-code/analyze.js +189 -0
- package/dist/adapters/claude-code/analyze.js.map +1 -0
- package/dist/adapters/claude-code/detect.js +60 -0
- package/dist/adapters/claude-code/detect.js.map +1 -0
- package/dist/adapters/claude-code/handlers.js +183 -0
- package/dist/adapters/claude-code/handlers.js.map +1 -0
- package/dist/adapters/claude-code/index.js +55 -0
- package/dist/adapters/claude-code/index.js.map +1 -0
- package/dist/adapters/claude-code/run-links.js +53 -0
- package/dist/adapters/claude-code/run-links.js.map +1 -0
- package/dist/adapters/claude-code/session.js +427 -0
- package/dist/adapters/claude-code/session.js.map +1 -0
- package/dist/adapters/claude-code/settings.js +161 -0
- package/dist/adapters/claude-code/settings.js.map +1 -0
- package/dist/adapters/claude-code/state.js +66 -0
- package/dist/adapters/claude-code/state.js.map +1 -0
- package/dist/adapters/claude-code/subagents.js +246 -0
- package/dist/adapters/claude-code/subagents.js.map +1 -0
- package/dist/adapters/claude-code/transcript.js +309 -0
- package/dist/adapters/claude-code/transcript.js.map +1 -0
- package/dist/adapters/codex/accumulator.js +135 -0
- package/dist/adapters/codex/accumulator.js.map +1 -0
- package/dist/adapters/codex/detect.js +93 -0
- package/dist/adapters/codex/detect.js.map +1 -0
- package/dist/adapters/codex/hooks-config.js +158 -0
- package/dist/adapters/codex/hooks-config.js.map +1 -0
- package/dist/adapters/codex/index.js +270 -0
- package/dist/adapters/codex/index.js.map +1 -0
- package/dist/adapters/codex/locate.js +72 -0
- package/dist/adapters/codex/locate.js.map +1 -0
- package/dist/adapters/codex/records.js +194 -0
- package/dist/adapters/codex/records.js.map +1 -0
- package/dist/adapters/codex/rollout.js +72 -0
- package/dist/adapters/codex/rollout.js.map +1 -0
- package/dist/adapters/codex/session.js +212 -0
- package/dist/adapters/codex/session.js.map +1 -0
- package/dist/adapters/codex/subagents.js +139 -0
- package/dist/adapters/codex/subagents.js.map +1 -0
- package/dist/adapters/codex/summarize.js +84 -0
- package/dist/adapters/codex/summarize.js.map +1 -0
- package/dist/adapters/codex/tools.js +145 -0
- package/dist/adapters/codex/tools.js.map +1 -0
- package/dist/adapters/hook-command.js +94 -0
- package/dist/adapters/hook-command.js.map +1 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/analysis/corrections.js +79 -0
- package/dist/analysis/corrections.js.map +1 -0
- package/dist/analysis/digest.js +16 -0
- package/dist/analysis/digest.js.map +1 -0
- package/dist/analysis/outcome.js +76 -0
- package/dist/analysis/outcome.js.map +1 -0
- package/dist/analysis/phases.js +203 -0
- package/dist/analysis/phases.js.map +1 -0
- package/dist/analysis/summaries.js +350 -0
- package/dist/analysis/summaries.js.map +1 -0
- package/dist/analysis/tokens.js +67 -0
- package/dist/analysis/tokens.js.map +1 -0
- package/dist/analysis/usage.js +229 -0
- package/dist/analysis/usage.js.map +1 -0
- package/dist/cc-cache.js +81 -0
- package/dist/cc-cache.js.map +1 -0
- package/dist/cli.js +128 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/common.js +235 -0
- package/dist/commands/common.js.map +1 -0
- package/dist/commands/doctor.js +193 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/flush.js +131 -0
- package/dist/commands/flush.js.map +1 -0
- package/dist/commands/init.js +119 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/link.js +171 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/live.js +130 -0
- package/dist/commands/live.js.map +1 -0
- package/dist/commands/login.js +397 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/status.js +155 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/token.js +92 -0
- package/dist/commands/token.js.map +1 -0
- package/dist/commands/uninstall.js +56 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/config.js +305 -0
- package/dist/config.js.map +1 -0
- package/dist/context.js +109 -0
- package/dist/context.js.map +1 -0
- package/dist/delivery.js +151 -0
- package/dist/delivery.js.map +1 -0
- package/dist/dotfile.js +187 -0
- package/dist/dotfile.js.map +1 -0
- package/dist/events.js +259 -0
- package/dist/events.js.map +1 -0
- package/dist/fsutil.js +50 -0
- package/dist/fsutil.js.map +1 -0
- package/dist/git.js +139 -0
- package/dist/git.js.map +1 -0
- package/dist/hook.js +147 -0
- package/dist/hook.js.map +1 -0
- package/dist/http.js +35 -0
- package/dist/http.js.map +1 -0
- package/dist/jobs.js +53 -0
- package/dist/jobs.js.map +1 -0
- package/dist/live/claude-code.js +226 -0
- package/dist/live/claude-code.js.map +1 -0
- package/dist/live/codex.js +91 -0
- package/dist/live/codex.js.map +1 -0
- package/dist/live/monitor.js +229 -0
- package/dist/live/monitor.js.map +1 -0
- package/dist/live/render.js +92 -0
- package/dist/live/render.js.map +1 -0
- package/dist/lock.js +93 -0
- package/dist/lock.js.map +1 -0
- package/dist/log.js +36 -0
- package/dist/log.js.map +1 -0
- package/dist/migrate.js +109 -0
- package/dist/migrate.js.map +1 -0
- package/dist/nudge.js +231 -0
- package/dist/nudge.js.map +1 -0
- package/dist/paths.js +73 -0
- package/dist/paths.js.map +1 -0
- package/dist/pk-cache.js +110 -0
- package/dist/pk-cache.js.map +1 -0
- package/dist/prices.js +45 -0
- package/dist/prices.js.map +1 -0
- package/dist/project-key.js +144 -0
- package/dist/project-key.js.map +1 -0
- package/dist/protocol-types.js +30 -0
- package/dist/protocol-types.js.map +1 -0
- package/dist/queue.js +649 -0
- package/dist/queue.js.map +1 -0
- package/dist/routing.js +122 -0
- package/dist/routing.js.map +1 -0
- package/dist/runtime.js +54 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sender.js +559 -0
- package/dist/sender.js.map +1 -0
- package/dist/state.js +59 -0
- package/dist/state.js.map +1 -0
- package/dist/ulid.js +73 -0
- package/dist/ulid.js.map +1 -0
- package/dist/version.js +26 -0
- package/dist/version.js.map +1 -0
- package/dist/warnings.js +71 -0
- package/dist/warnings.js.map +1 -0
- package/dist/worker-main.js +14 -0
- package/dist/worker-main.js.map +1 -0
- package/dist/worker.js +295 -0
- package/dist/worker.js.map +1 -0
- package/package.json +25 -4
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { PHASES } from '../protocol-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Phase classification (BRIEF §5.4), no LLM. Bump PHASE_RULES_VERSION whenever a rule changes the
|
|
4
|
+
* result for some input; it is stored per turn so dashboards can tell rule generations apart.
|
|
5
|
+
*/
|
|
6
|
+
export const PHASE_RULES_VERSION = 1;
|
|
7
|
+
const DOC_FILE_RE = /(^|\/)(docs?|documentation)\/|(^|\/)(readme|changelog|changes|history|contributing|authors|license|notice)$|\.(md|mdx|markdown|rst|txt|adoc|asciidoc)$/i;
|
|
8
|
+
export function isDocumentationPath(path) {
|
|
9
|
+
return DOC_FILE_RE.test(path.replace(/\\/g, '/'));
|
|
10
|
+
}
|
|
11
|
+
const norm = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
12
|
+
const DELEGATE_TOOLS = new Set(['task', 'agent', 'spawnagent', 'subagent', 'delegate', 'runagent']);
|
|
13
|
+
const EXPLORE_TOOLS = new Set([
|
|
14
|
+
'read', 'grep', 'glob', 'ls', 'list', 'listdir', 'listfiles', 'readfile', 'viewfile', 'view', 'search',
|
|
15
|
+
'websearch', 'webfetch', 'fetch', 'findfiles', 'filesearch', 'codesearch', 'readmanyfiles', 'notebookread', 'viewimage',
|
|
16
|
+
]);
|
|
17
|
+
const EDIT_TOOLS = new Set([
|
|
18
|
+
'edit', 'multiedit', 'write', 'writefile', 'notebookedit', 'applypatch', 'patch', 'createfile', 'strreplace',
|
|
19
|
+
'strreplacebasededittool', 'replace', 'editfile', 'insert',
|
|
20
|
+
]);
|
|
21
|
+
const SHELL_TOOLS = new Set(['bash', 'shell', 'localshell', 'execcommand', 'exec', 'runcommand', 'terminal', 'unifiedexec', 'powershell']);
|
|
22
|
+
// ---------------------------------------------------------------- shell commands
|
|
23
|
+
const TEST_RE = [
|
|
24
|
+
/^(npm|pnpm|yarn|bun)( run)? test(:|\b)/,
|
|
25
|
+
/^(npm|pnpm|yarn|bun) (-[^\s]+ |--filter[= ][^\s]+ |-C [^\s]+ )*(run )?test(:|\b)/,
|
|
26
|
+
/^(npx |pnpm |pnpm exec |yarn |bunx |bun x )?(vitest|jest|mocha|ava|tap|karma|playwright test|cypress run|jasmine)\b(?! (--version|init))/,
|
|
27
|
+
/^bun test\b/,
|
|
28
|
+
/^(python3?|py)( -m)? (pytest|unittest|nose2?)\b/,
|
|
29
|
+
/^(pytest|py\.test|tox|nox|rspec|phpunit|pest|ctest|behave)\b/,
|
|
30
|
+
/^(poetry|uv|pipenv|hatch|pdm) run (pytest|tox|nox|python -m pytest)\b/,
|
|
31
|
+
/^go test\b/,
|
|
32
|
+
/^cargo (test|nextest)\b/,
|
|
33
|
+
/^(xcodebuild\b.*\btest\b|swift test\b)/,
|
|
34
|
+
/^(mvn|\.\/mvnw)\b.*\b(test|verify)\b/,
|
|
35
|
+
/^(gradle|\.\/gradlew)\b.*\b(test|check)\b/,
|
|
36
|
+
/^dotnet test\b/,
|
|
37
|
+
/^deno test\b/,
|
|
38
|
+
/^(bundle exec )?(rake test|rails test|rspec)\b/,
|
|
39
|
+
/^mix test\b/,
|
|
40
|
+
/^make (test|check)\b/,
|
|
41
|
+
/^flutter test\b/,
|
|
42
|
+
];
|
|
43
|
+
const VALIDATE_RE = [
|
|
44
|
+
/^(npx |pnpm |pnpm exec |yarn |bunx )?(eslint|tsc|tsgo|prettier --check|biome (check|lint|ci)|oxlint|stylelint|vue-tsc|svelte-check|next lint|astro check)\b/,
|
|
45
|
+
/^(npm|pnpm|yarn|bun) (-[^\s]+ |--filter[= ][^\s]+ |-C [^\s]+ )*(run )?(build|lint|typecheck|type-check|check-types|tsc|check|format:check|lint:\S+|typecheck:\S+)(\s|$)/,
|
|
46
|
+
/^(npx |pnpm |yarn )?(next|vite|webpack|rollup|esbuild|tsup|turbo run build|turbo build|nx build)( build)?\b/,
|
|
47
|
+
/^(ruff|mypy|pyright|flake8|pylint|black --check|isort --check|bandit)\b/,
|
|
48
|
+
/^(python3?|py) -m (mypy|pyright|flake8|pylint|ruff|compileall|py_compile)\b/,
|
|
49
|
+
/^cargo (check|clippy|build|fmt -- --check|fmt --check)\b/,
|
|
50
|
+
/^(swift build|swiftlint|xcodebuild\b)/,
|
|
51
|
+
/^go (build|vet)\b|^(golangci-lint|staticcheck|gofmt -l)\b/,
|
|
52
|
+
/^(mvn|\.\/mvnw)\b.*\b(compile|package|install)\b/,
|
|
53
|
+
/^(gradle|\.\/gradlew)\b.*\b(build|assemble|compile\w*|lint)\b/,
|
|
54
|
+
/^dotnet (build|format --verify)\b/,
|
|
55
|
+
/^(shellcheck|hadolint|terraform validate|tflint|actionlint|yamllint|markdownlint|rubocop|phpstan|psalm|ktlint|detekt)\b/,
|
|
56
|
+
/^make( (build|lint|all|check-types))?$/,
|
|
57
|
+
/^make (build|lint)\b/,
|
|
58
|
+
/^deno (check|lint)\b/,
|
|
59
|
+
];
|
|
60
|
+
const EXPLORE_RE = [
|
|
61
|
+
/^(cat|head|tail|less|more|bat|rg|ag|ack|grep|egrep|fgrep|find|fd|ls|tree|exa|eza|wc|file|stat|du|realpath|readlink|jq|yq|nl|od|xxd|hexdump|strings|diff|cmp|column|sort|uniq|cut|tr|awk|which|whereis|type)\b/,
|
|
62
|
+
/^sed -n\b/,
|
|
63
|
+
/^(curl|wget|http|xh)\b/,
|
|
64
|
+
];
|
|
65
|
+
const CODING_RE = [/^sed -i\b/, /^(perl -p?i|perl -i)\b/, /^(patch|tee|touch|mkdir|cp|mv|rm|ln)\b/];
|
|
66
|
+
const PRIORITY = ['testing', 'validating', 'coding', 'documenting', 'exploring', 'other'];
|
|
67
|
+
/** Splits a command line into simple commands (&&, ||, ;, |, newlines) and strips env/cd/time/sudo prefixes. */
|
|
68
|
+
export function splitShellCommand(command) {
|
|
69
|
+
return command
|
|
70
|
+
.split(/&&|\|\||;|\||\n/)
|
|
71
|
+
.map((part) => part
|
|
72
|
+
.trim()
|
|
73
|
+
.replace(/^[({\s]+|[)}\s]+$/g, '')
|
|
74
|
+
.replace(/^(\w+=("[^"]*"|'[^']*'|\S*)\s+)+/, '')
|
|
75
|
+
.replace(/^(sudo|time|nice|exec|command|env|npx --yes|npx -y|timeout \d+[smh]?)\s+/, '')
|
|
76
|
+
.replace(/^(\w+=("[^"]*"|'[^']*'|\S*)\s+)+/, '')
|
|
77
|
+
.toLowerCase())
|
|
78
|
+
.filter((p) => p && !/^cd\b/.test(p));
|
|
79
|
+
}
|
|
80
|
+
function unwrapShell(command) {
|
|
81
|
+
if (typeof command === 'string')
|
|
82
|
+
return command;
|
|
83
|
+
// Codex style argv: ["bash", "-lc", "npm test"]
|
|
84
|
+
const [first, flag, script] = command;
|
|
85
|
+
if (first && /(^|\/)(ba|z|da|fi)?sh$|powershell|pwsh/.test(first) && flag && /^-\w*c$/i.test(flag) && script)
|
|
86
|
+
return script;
|
|
87
|
+
return command.join(' ');
|
|
88
|
+
}
|
|
89
|
+
function classifySimpleCommand(cmd) {
|
|
90
|
+
if (TEST_RE.some((r) => r.test(cmd)))
|
|
91
|
+
return 'testing';
|
|
92
|
+
if (VALIDATE_RE.some((r) => r.test(cmd)))
|
|
93
|
+
return 'validating';
|
|
94
|
+
if (CODING_RE.some((r) => r.test(cmd)))
|
|
95
|
+
return 'coding';
|
|
96
|
+
if (EXPLORE_RE.some((r) => r.test(cmd)))
|
|
97
|
+
return 'exploring';
|
|
98
|
+
return 'other';
|
|
99
|
+
}
|
|
100
|
+
/** Phase of a shell command; compound commands take the highest-priority part (testing > validating > coding > exploring > other). */
|
|
101
|
+
export function classifyCommand(command) {
|
|
102
|
+
const parts = splitShellCommand(unwrapShell(command));
|
|
103
|
+
let best = 'other';
|
|
104
|
+
for (const part of parts) {
|
|
105
|
+
const p = classifySimpleCommand(part);
|
|
106
|
+
if (PRIORITY.indexOf(p) < PRIORITY.indexOf(best))
|
|
107
|
+
best = p;
|
|
108
|
+
}
|
|
109
|
+
return best === 'delegating' ? 'other' : best;
|
|
110
|
+
}
|
|
111
|
+
/** Whether a shell command is a test or validate run (its exit code feeds the outcome, BRIEF §5.5). */
|
|
112
|
+
export function checkKind(command) {
|
|
113
|
+
const p = classifyCommand(command);
|
|
114
|
+
return p === 'testing' || p === 'validating' ? p : null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Classifies one tool call. Tool names are matched case- and punctuation-insensitively, so Claude
|
|
118
|
+
* Code (`Read`, `MultiEdit`, `Task`) and Codex (`shell`, `apply_patch`, `web_search`) share rules.
|
|
119
|
+
* MCP tools (`mcp__…`) and unknown tools are `other`.
|
|
120
|
+
*/
|
|
121
|
+
export function classify(toolName, info = {}) {
|
|
122
|
+
if (/^mcp__/i.test(toolName))
|
|
123
|
+
return 'other';
|
|
124
|
+
const name = norm(toolName);
|
|
125
|
+
if (DELEGATE_TOOLS.has(name))
|
|
126
|
+
return 'delegating';
|
|
127
|
+
if (EXPLORE_TOOLS.has(name))
|
|
128
|
+
return 'exploring';
|
|
129
|
+
if (EDIT_TOOLS.has(name)) {
|
|
130
|
+
const paths = info.filePaths ?? [];
|
|
131
|
+
return paths.length && paths.every(isDocumentationPath) ? 'documenting' : 'coding';
|
|
132
|
+
}
|
|
133
|
+
if (SHELL_TOOLS.has(name))
|
|
134
|
+
return info.command ? classifyCommand(info.command) : 'other';
|
|
135
|
+
return 'other';
|
|
136
|
+
}
|
|
137
|
+
export const emptyPhaseTotals = () => ({
|
|
138
|
+
thinking: 0, exploring: 0, coding: 0, documenting: 0, testing: 0, validating: 0, other: 0, delegating: 0,
|
|
139
|
+
});
|
|
140
|
+
/**
|
|
141
|
+
* Assigns the time between consecutive entries to a phase (BRIEF §5.4):
|
|
142
|
+
* - while tool calls are pending → the pending tool's phase (`delegating` wins if any sub-agent is running);
|
|
143
|
+
* - otherwise the model is generating → the phase of the tool call that ends the interval, or
|
|
144
|
+
* `thinking` when the interval ends with plain output (incl. thinking blocks, final answer).
|
|
145
|
+
* `endAt` (e.g. the Stop time) extends the last interval. Negative gaps are ignored.
|
|
146
|
+
*/
|
|
147
|
+
export function attributeTime(entries, endAt) {
|
|
148
|
+
const totals = emptyPhaseTotals();
|
|
149
|
+
const pending = new Map();
|
|
150
|
+
const timeline = [...entries];
|
|
151
|
+
if (endAt !== undefined && timeline.length && endAt > (timeline.at(-1)?.at ?? endAt)) {
|
|
152
|
+
timeline.push({ at: endAt, type: 'output' });
|
|
153
|
+
}
|
|
154
|
+
for (let i = 1; i < timeline.length; i++) {
|
|
155
|
+
const prev = timeline[i - 1];
|
|
156
|
+
const cur = timeline[i];
|
|
157
|
+
// apply the effect of `prev` on the pending set
|
|
158
|
+
if (prev.type === 'tool_call') {
|
|
159
|
+
pending.delete(prev.id);
|
|
160
|
+
pending.set(prev.id, prev.phase);
|
|
161
|
+
}
|
|
162
|
+
else if (prev.type === 'tool_result')
|
|
163
|
+
pending.delete(prev.id);
|
|
164
|
+
else if (prev.type === 'prompt')
|
|
165
|
+
pending.clear();
|
|
166
|
+
const gap = cur.at - prev.at;
|
|
167
|
+
if (!(gap > 0))
|
|
168
|
+
continue;
|
|
169
|
+
let phase;
|
|
170
|
+
if (pending.size) {
|
|
171
|
+
// Calls emitted back-to-back: the gap before the next call is that call's generation time.
|
|
172
|
+
if (cur.type === 'tool_call' && prev.type === 'tool_call')
|
|
173
|
+
phase = cur.phase;
|
|
174
|
+
else {
|
|
175
|
+
const running = [...pending.values()];
|
|
176
|
+
phase = running.includes('delegating') ? 'delegating' : (running.at(-1) ?? 'other');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
phase = cur.type === 'tool_call' ? cur.phase : 'thinking';
|
|
181
|
+
}
|
|
182
|
+
totals[phase] += gap;
|
|
183
|
+
}
|
|
184
|
+
return totals;
|
|
185
|
+
}
|
|
186
|
+
/** Wire format: drops `delegating` (counted in the child run) and zero phases; rounds to whole ms. */
|
|
187
|
+
export function toPhasesMs(totals) {
|
|
188
|
+
const out = {};
|
|
189
|
+
for (const p of PHASES) {
|
|
190
|
+
const v = Math.round(totals[p] ?? 0);
|
|
191
|
+
if (v > 0)
|
|
192
|
+
out[p] = v;
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
/** Adds phase totals (e.g. across runs). */
|
|
197
|
+
export function addPhaseTotals(a, b) {
|
|
198
|
+
const out = { ...a };
|
|
199
|
+
for (const k of Object.keys(out))
|
|
200
|
+
out[k] += b[k] ?? 0;
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=phases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phases.js","sourceRoot":"","sources":["../../src/analysis/phases.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { cleanSummaryText, turnSummaryEvent } from '../events.js';
|
|
5
|
+
import { errorMessage, readJsonFile, withTimeout, writeJsonAtomic } from '../fsutil.js';
|
|
6
|
+
import { SUMMARIZING_ENV } from '../protocol-types.js';
|
|
7
|
+
import { ulid } from '../ulid.js';
|
|
8
|
+
export const SUMMARY_TIMEOUT_MS = 120_000;
|
|
9
|
+
/** A chunk whose summarize call has not settled after this long counts as a failed attempt (the command itself is killed at SUMMARY_TIMEOUT_MS). */
|
|
10
|
+
export const SUMMARY_CHUNK_TIMEOUT_MS = SUMMARY_TIMEOUT_MS + 10_000;
|
|
11
|
+
/** Turns per headless call; a normal session fits in one call. */
|
|
12
|
+
export const SUMMARY_CHUNK = 25;
|
|
13
|
+
const MAX_ATTEMPTS = 5;
|
|
14
|
+
const MAX_PENDING_AGE_MS = 7 * 24 * 3600_000;
|
|
15
|
+
const MAX_PENDING = 200;
|
|
16
|
+
/** Summarize mid-session once this many turns are waiting (long sessions). */
|
|
17
|
+
export const SUMMARY_BATCH = 10;
|
|
18
|
+
/** … or once the oldest waiting turn has waited this long (ADR-0023: a dashboard turn should not stay without a summary for hours). */
|
|
19
|
+
export const SUMMARY_MAX_WAIT_MS = 10 * 60_000;
|
|
20
|
+
/**
|
|
21
|
+
* Whether an adapter should hand its pending digests to the summarizer now (ADR-0023): at session end, with
|
|
22
|
+
* SUMMARY_BATCH or more turns waiting, or when the first of them has waited SUMMARY_MAX_WAIT_MS (`since` = the hook
|
|
23
|
+
* time at which the adapter first saw digests pending; undefined = none pending yet). Pure.
|
|
24
|
+
*/
|
|
25
|
+
export function summariesDue(s) {
|
|
26
|
+
if (s.pending <= 0)
|
|
27
|
+
return false;
|
|
28
|
+
if (s.ended || s.pending >= SUMMARY_BATCH)
|
|
29
|
+
return true;
|
|
30
|
+
return s.since !== undefined && s.now - s.since >= SUMMARY_MAX_WAIT_MS;
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------- prompt
|
|
33
|
+
/**
|
|
34
|
+
* ONE batched prompt for all digests. Turns get short ids (t1, t2, …) so the model cannot mangle real
|
|
35
|
+
* turn ids; `ids` maps them back.
|
|
36
|
+
*/
|
|
37
|
+
export function buildSummaryPrompt(digests) {
|
|
38
|
+
const ids = new Map();
|
|
39
|
+
const turns = digests.map((d, i) => {
|
|
40
|
+
const shortId = `t${i + 1}`;
|
|
41
|
+
ids.set(shortId, d.turnId);
|
|
42
|
+
return { id: shortId, prompt: d.prompt, tools: d.tools, last_message: d.lastAssistantMessage };
|
|
43
|
+
});
|
|
44
|
+
const prompt = [
|
|
45
|
+
'You write one-line summaries of AI coding-agent turns for a usage dashboard.',
|
|
46
|
+
'For EACH turn below write what was done, in English, at most 140 characters, past tense,',
|
|
47
|
+
'e.g. "Added JWT refresh to auth middleware". Do not include file paths, code, URLs, secrets or names of people.',
|
|
48
|
+
'Reply with ONLY a JSON object that maps every turn id to its summary, e.g. {"t1": "...", "t2": "..."}. No other text.',
|
|
49
|
+
'',
|
|
50
|
+
'Turns (JSON):',
|
|
51
|
+
JSON.stringify(turns),
|
|
52
|
+
].join('\n');
|
|
53
|
+
return { prompt, ids };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Path-looking words → their last segment, so a stray path in a summary does not leave the machine.
|
|
57
|
+
* A word counts as a path when it starts with `/`, `~`, `.` or a drive letter, has two or more
|
|
58
|
+
* separators, or ends in a file name with an extension (`src/auth.ts`). URLs and `and/or` are kept.
|
|
59
|
+
*/
|
|
60
|
+
export function scrubPaths(s) {
|
|
61
|
+
return s
|
|
62
|
+
.split(/(\s+)/)
|
|
63
|
+
.map((word) => {
|
|
64
|
+
if (!/[\\/]/.test(word) || /^[a-z][a-z0-9+.-]*:\/\//i.test(word))
|
|
65
|
+
return word;
|
|
66
|
+
const m = /^(["'(`]*)(.*?)([)"'`.,;:!?]*)$/.exec(word);
|
|
67
|
+
const [, pre = '', core = '', post = ''] = m ?? [];
|
|
68
|
+
const parts = core.split(/[\\/]+/).filter(Boolean);
|
|
69
|
+
const last = parts.at(-1) ?? '';
|
|
70
|
+
const isPath = /^(?:[\\/]|~|\.{1,2}[\\/]|[a-z]:[\\/])/i.test(core) ||
|
|
71
|
+
(core.match(/[\\/]/g)?.length ?? 0) >= 2 ||
|
|
72
|
+
/\.[a-z0-9]{1,6}$/i.test(last);
|
|
73
|
+
return isPath && last ? `${pre}${last}${post}` : word;
|
|
74
|
+
})
|
|
75
|
+
.join('');
|
|
76
|
+
}
|
|
77
|
+
function extractJson(text) {
|
|
78
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text);
|
|
79
|
+
const candidates = [fenced?.[1], text];
|
|
80
|
+
for (const c of candidates) {
|
|
81
|
+
if (!c)
|
|
82
|
+
continue;
|
|
83
|
+
for (const [open, close] of [['{', '}'], ['[', ']']]) {
|
|
84
|
+
const a = c.indexOf(open);
|
|
85
|
+
const b = c.lastIndexOf(close);
|
|
86
|
+
if (a >= 0 && b > a) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(c.slice(a, b + 1));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* try next */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parses the model's reply robustly: a JSON object `{"t1": "…"}` (optionally in a ``` fence or
|
|
100
|
+
* surrounded by prose), an array of `{id, summary}`, or `t1: …` lines. Returns turnId → summary
|
|
101
|
+
* (cleaned, path-scrubbed, ≤ 140 chars). Unknown ids are ignored.
|
|
102
|
+
*/
|
|
103
|
+
export function parseSummaryResponse(text, ids) {
|
|
104
|
+
const out = new Map();
|
|
105
|
+
const put = (shortId, summary) => {
|
|
106
|
+
const turnId = typeof shortId === 'string' ? ids.get(shortId.trim()) : undefined;
|
|
107
|
+
if (!turnId || typeof summary !== 'string')
|
|
108
|
+
return;
|
|
109
|
+
const clean = cleanSummaryText(scrubPaths(summary).replace(/^["'`]+|["'`]+$/g, ''));
|
|
110
|
+
if (clean)
|
|
111
|
+
out.set(turnId, clean);
|
|
112
|
+
};
|
|
113
|
+
const json = extractJson(text);
|
|
114
|
+
if (Array.isArray(json)) {
|
|
115
|
+
for (const item of json) {
|
|
116
|
+
if (item && typeof item === 'object') {
|
|
117
|
+
const o = item;
|
|
118
|
+
put(o.id ?? o.turn ?? o.turn_id, o.summary ?? o.text);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (json && typeof json === 'object') {
|
|
123
|
+
const obj = json;
|
|
124
|
+
const inner = obj.summaries && typeof obj.summaries === 'object' ? obj.summaries : obj;
|
|
125
|
+
for (const [k, v] of Object.entries(inner))
|
|
126
|
+
put(k, v);
|
|
127
|
+
}
|
|
128
|
+
if (!out.size) {
|
|
129
|
+
for (const m of text.matchAll(/^\s*[-*]?\s*"?(t\d+)"?\s*[:=\-–]\s*(.+?)\s*,?\s*$/gim))
|
|
130
|
+
put(m[1], m[2]);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Runs a headless harness command (e.g. `claude -p --model haiku`) with AI_OBS_SUMMARIZING=1 so our
|
|
136
|
+
* hooks exit immediately inside it (recursion guard). Prompt goes to stdin when `input` is set.
|
|
137
|
+
* Own process group, so a timeout (default 120 s) kills the whole tree. Never throws.
|
|
138
|
+
*/
|
|
139
|
+
export function runSummaryCommand(opts) {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const [cmd, ...args] = opts.argv;
|
|
142
|
+
if (!cmd)
|
|
143
|
+
return resolve({ ok: false, stdout: '', code: null, timedOut: false, error: 'empty command' });
|
|
144
|
+
let child;
|
|
145
|
+
try {
|
|
146
|
+
child = spawn(cmd, args, {
|
|
147
|
+
cwd: opts.cwd,
|
|
148
|
+
env: { ...process.env, ...opts.env, [SUMMARIZING_ENV]: '1' },
|
|
149
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
150
|
+
detached: process.platform !== 'win32',
|
|
151
|
+
windowsHide: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
return resolve({ ok: false, stdout: '', code: null, timedOut: false, error: errorMessage(err) });
|
|
156
|
+
}
|
|
157
|
+
let stdout = '';
|
|
158
|
+
let stderr = '';
|
|
159
|
+
let timedOut = false;
|
|
160
|
+
let settled = false;
|
|
161
|
+
const finish = (r) => {
|
|
162
|
+
if (settled)
|
|
163
|
+
return;
|
|
164
|
+
settled = true;
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
resolve(r);
|
|
167
|
+
};
|
|
168
|
+
const timer = setTimeout(() => {
|
|
169
|
+
timedOut = true;
|
|
170
|
+
try {
|
|
171
|
+
if (child.pid && process.platform !== 'win32')
|
|
172
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
173
|
+
else
|
|
174
|
+
child.kill('SIGKILL');
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
child.kill('SIGKILL');
|
|
178
|
+
}
|
|
179
|
+
}, opts.timeoutMs ?? SUMMARY_TIMEOUT_MS);
|
|
180
|
+
child.stdout?.on('data', (c) => {
|
|
181
|
+
if (stdout.length < 1_000_000)
|
|
182
|
+
stdout += c.toString('utf8');
|
|
183
|
+
});
|
|
184
|
+
child.stderr?.on('data', (c) => {
|
|
185
|
+
if (stderr.length < 2000)
|
|
186
|
+
stderr += c.toString('utf8');
|
|
187
|
+
});
|
|
188
|
+
child.on('error', (err) => finish({ ok: false, stdout, code: null, timedOut, error: errorMessage(err) }));
|
|
189
|
+
child.on('close', (code) => finish({
|
|
190
|
+
ok: code === 0 && !timedOut,
|
|
191
|
+
stdout,
|
|
192
|
+
code,
|
|
193
|
+
timedOut,
|
|
194
|
+
...(code === 0 && !timedOut ? {} : { error: timedOut ? 'timeout' : `exit ${code}: ${stderr.slice(0, 200)}` }),
|
|
195
|
+
}));
|
|
196
|
+
child.stdin?.on('error', () => { });
|
|
197
|
+
child.stdin?.end(opts.input ?? '');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Convenience for HarnessAdapter.summarize(): builds the batched prompt, runs the command, parses.
|
|
202
|
+
* `command(prompt)` returns argv plus whether the prompt goes on stdin (`input`) or in argv.
|
|
203
|
+
* `extractText` unwraps harness output formats (e.g. a JSON envelope) before parsing.
|
|
204
|
+
*/
|
|
205
|
+
export async function summarizeWithCommand(digests, opts) {
|
|
206
|
+
if (!digests.length)
|
|
207
|
+
return new Map();
|
|
208
|
+
const { prompt, ids } = buildSummaryPrompt(digests);
|
|
209
|
+
const cmd = opts.command(prompt);
|
|
210
|
+
const res = await runSummaryCommand({ ...cmd, timeoutMs: opts.timeoutMs, cwd: opts.cwd });
|
|
211
|
+
if (!res.ok)
|
|
212
|
+
throw new Error(`summary command failed: ${res.error ?? 'unknown'}`);
|
|
213
|
+
return parseSummaryResponse(opts.extractText ? opts.extractText(res.stdout) : res.stdout, ids);
|
|
214
|
+
}
|
|
215
|
+
/** Failed digests (local only, mode 0600): ~/.ai-obs/state/summaries/<harness>.json */
|
|
216
|
+
export function pendingSummariesPath(stateDir, harness) {
|
|
217
|
+
return join(stateDir, 'summaries', `${harness}.json`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Digests due for summarising, written by the hook's job and read by the summary phase after the worker's flush
|
|
221
|
+
* (ADR-0027): ~/.ai-obs/state/summaries/staged/<harness>/<ulid>.json (local only, mode 0600).
|
|
222
|
+
*/
|
|
223
|
+
export function stagedSummariesDir(stateDir, harness) {
|
|
224
|
+
return join(stateDir, 'summaries', 'staged', harness);
|
|
225
|
+
}
|
|
226
|
+
const STAGED_RE = /^[0-9A-HJKMNP-TV-Z]{26}\.json$/;
|
|
227
|
+
/** One staged file per call (attempts 0, firstFailedAt = nowMs, key). Null key: logged, nothing written. Empty: no-op. Throws on write failure. */
|
|
228
|
+
export function stageDigests(stateDir, harness, digests, key, nowMs, log) {
|
|
229
|
+
if (!digests.length)
|
|
230
|
+
return;
|
|
231
|
+
if (!key) {
|
|
232
|
+
log.info('summaries: session not recorded, digests dropped', { harness, count: digests.length });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const staged = digests.map((d) => ({ ...d, attempts: 0, firstFailedAt: nowMs, key }));
|
|
236
|
+
writeJsonAtomic(join(stagedSummariesDir(stateDir, harness), `${ulid(nowMs)}.json`), staged);
|
|
237
|
+
}
|
|
238
|
+
/** Staged file paths, oldest first (ulid names sort by time); [] when none. */
|
|
239
|
+
export function listStagedDigests(stateDir, harness) {
|
|
240
|
+
const dir = stagedSummariesDir(stateDir, harness);
|
|
241
|
+
try {
|
|
242
|
+
return readdirSync(dir)
|
|
243
|
+
.filter((n) => STAGED_RE.test(n))
|
|
244
|
+
.sort()
|
|
245
|
+
.map((n) => join(dir, n));
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Number of staged digests (sum of file lengths). */
|
|
252
|
+
export function countStagedDigests(stateDir, harness) {
|
|
253
|
+
return listStagedDigests(stateDir, harness).reduce((n, f) => {
|
|
254
|
+
const v = readJsonFile(f);
|
|
255
|
+
return n + (Array.isArray(v) ? v.length : 0);
|
|
256
|
+
}, 0);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Summarizes the staged digests plus previously failed ones and hands the `turn_summary` events to
|
|
260
|
+
* `opts.commit`, grouped by stream (ADR-0027). Only after a successful commit the digests still missing are
|
|
261
|
+
* stored for the next run (max 5 attempts / 7 days, staged ones too) and the staged files are deleted, so a crash or a refused
|
|
262
|
+
* commit loses nothing: the next run summarises them again, with the same event ids (the time is the digest's
|
|
263
|
+
* `firstFailedAt`). Pending digests without a stream (0.1.x) are dropped. Digests never leave the machine.
|
|
264
|
+
* Null when there was nothing staged (and no `retry`) or the commit was refused. Run under summaries.lock.
|
|
265
|
+
*/
|
|
266
|
+
export async function runSummaryBatch(adapter, ctx, opts) {
|
|
267
|
+
const files = listStagedDigests(ctx.paths.state, adapter.name);
|
|
268
|
+
if (!files.length && !opts.retry)
|
|
269
|
+
return null;
|
|
270
|
+
const file = pendingSummariesPath(ctx.paths.state, adapter.name);
|
|
271
|
+
const nowMs = ctx.now().getTime();
|
|
272
|
+
const pending = new Map();
|
|
273
|
+
let unkeyed = 0;
|
|
274
|
+
for (const p of readJsonFile(file) ?? []) {
|
|
275
|
+
if (!p.key) {
|
|
276
|
+
unkeyed++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (p.attempts < MAX_ATTEMPTS && nowMs - p.firstFailedAt < MAX_PENDING_AGE_MS)
|
|
280
|
+
pending.set(p.turnId, p);
|
|
281
|
+
}
|
|
282
|
+
if (unkeyed)
|
|
283
|
+
ctx.log.warn('summaries: pending digests without a stream dropped', { harness: adapter.name, count: unkeyed });
|
|
284
|
+
const staged = [];
|
|
285
|
+
for (const f of files) {
|
|
286
|
+
const list = readJsonFile(f);
|
|
287
|
+
if (!Array.isArray(list)) {
|
|
288
|
+
ctx.log.warn('summaries: unreadable staged digests dropped', { harness: adapter.name });
|
|
289
|
+
rmSync(f, { force: true });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
staged.push(f);
|
|
293
|
+
for (const d of list) {
|
|
294
|
+
if (!d || typeof d.turnId !== 'string' || !d.key)
|
|
295
|
+
continue;
|
|
296
|
+
const prev = pending.get(d.turnId);
|
|
297
|
+
const firstFailedAt = prev?.firstFailedAt ?? d.firstFailedAt;
|
|
298
|
+
if (!(nowMs - firstFailedAt < MAX_PENDING_AGE_MS))
|
|
299
|
+
continue; // a staged digest expires like a pending one
|
|
300
|
+
pending.set(d.turnId, { ...d, attempts: prev?.attempts ?? 0, firstFailedAt, key: d.key });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const all = [...pending.values()];
|
|
304
|
+
const byKey = new Map();
|
|
305
|
+
let summarized = 0;
|
|
306
|
+
for (let i = 0; i < all.length; i += SUMMARY_CHUNK) {
|
|
307
|
+
// past the deadline the rest waits in the pending file, so the work done so far is committed, not thrown away
|
|
308
|
+
if (opts.deadline !== undefined && Date.now() >= opts.deadline)
|
|
309
|
+
break;
|
|
310
|
+
const chunk = all.slice(i, i + SUMMARY_CHUNK);
|
|
311
|
+
let result = new Map();
|
|
312
|
+
try {
|
|
313
|
+
result = await withTimeout(adapter.summarize(chunk.map(({ attempts: _a, firstFailedAt: _f, key: _k, ...d }) => d), ctx), opts.chunkTimeoutMs ?? SUMMARY_CHUNK_TIMEOUT_MS, `${adapter.name} summarize`);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
ctx.log.warn('summaries: summarize failed, will retry', { harness: adapter.name, turns: chunk.length, error: errorMessage(err).slice(0, 200) });
|
|
317
|
+
}
|
|
318
|
+
for (const d of chunk) {
|
|
319
|
+
const text = result.get(d.turnId);
|
|
320
|
+
// stable id: a re-run after a refused commit or a crash re-sends the same event (ADR-0011)
|
|
321
|
+
const ev = text ? turnSummaryEvent({ harness: adapter.name, turnId: d.turnId, summary: text, at: d.firstFailedAt }) : null;
|
|
322
|
+
if (ev && d.key) {
|
|
323
|
+
const tag = JSON.stringify(d.key);
|
|
324
|
+
const group = byKey.get(tag) ?? { key: d.key, events: [] };
|
|
325
|
+
group.events.push(ev);
|
|
326
|
+
byKey.set(tag, group);
|
|
327
|
+
summarized++;
|
|
328
|
+
pending.delete(d.turnId);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
d.attempts += 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!(await opts.commit([...byKey.values()])))
|
|
336
|
+
return null;
|
|
337
|
+
const left = [...pending.values()].filter((p) => p.attempts < MAX_ATTEMPTS).slice(-MAX_PENDING);
|
|
338
|
+
try {
|
|
339
|
+
writeJsonAtomic(file, left);
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
// the staged files stay: the next run re-sends their summaries under the same ids
|
|
343
|
+
ctx.log.error('summaries: cannot store pending digests', { error: errorMessage(err) });
|
|
344
|
+
return { summarized, pending: left.length };
|
|
345
|
+
}
|
|
346
|
+
for (const f of staged)
|
|
347
|
+
rmSync(f, { force: true });
|
|
348
|
+
return { summarized, pending: left.length };
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=summaries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summaries.js","sourceRoot":"","sources":["../../src/analysis/summaries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAExF,OAAO,EAAE,eAAe,EAAyB,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGlC,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAC1C,oJAAoJ;AACpJ,MAAM,CAAC,MAAM,wBAAwB,GAAG,kBAAkB,GAAG,MAAM,CAAC;AACpE,kEAAkE;AAClE,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAChC,MAAM,YAAY,GAAG,CAAC,CAAC;AACvB,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC;AAC7C,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB,8EAA8E;AAC9E,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAChC,uIAAuI;AACvI,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,GAAG,MAAM,CAAC;AAE/C;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,CAA8E;IACzG,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACjC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,IAAI,aAAa;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,IAAI,mBAAmB,CAAC;AACzE,CAAC;AAED,0EAA0E;AAE1E;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAA8B;IAC/D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QAC3B,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC,oBAAoB,EAAE,CAAC;IACjG,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG;QACb,8EAA8E;QAC9E,0FAA0F;QAC1F,iHAAiH;QACjH,uHAAuH;QACvH,EAAE;QACF,eAAe;QACf,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;KACtB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,OAAO,CAAC;SACL,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAC9E,MAAM,CAAC,GAAG,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,CAAC,EAAE,GAAG,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAChC,MAAM,MAAM,GACV,wCAAwC,CAAC,IAAI,CAAC,IAAI,CAAC;YACnD,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC;YACxC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,MAAM,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAU,EAAE,CAAC;YAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1B,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC;oBACH,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACvC,CAAC;gBAAC,MAAM,CAAC;oBACP,cAAc;gBAChB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,GAAgC;IACjF,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,MAAM,GAAG,GAAG,CAAC,OAAgB,EAAE,OAAgB,EAAE,EAAE;QACjD,MAAM,MAAM,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjF,IAAI,CAAC,MAAM,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO;QACnD,MAAM,KAAK,GAAG,gBAAgB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC,CAAC;QACpF,IAAI,KAAK;YAAE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC;IACF,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,MAAM,CAAC,GAAG,IAA+B,CAAC;gBAC1C,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;IACH,CAAC;SAAM,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,IAA+B,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAE,GAAG,CAAC,SAAqC,CAAC,CAAC,CAAC,GAAG,CAAC;QACpH,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,sDAAsD,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzG,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAYD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAMjC;IACC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QACjC,IAAI,CAAC,GAAG;YAAE,OAAO,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;QACzG,IAAI,KAA+B,CAAC;QACpC,IAAI,CAAC;YACH,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE;gBACvB,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,GAAG,EAAE;gBAC5D,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;gBAC/B,QAAQ,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;gBACtC,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC;QACD,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,CAAmB,EAAE,EAAE;YACrC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,QAAQ,GAAG,IAAI,CAAC;YAChB,IAAI,CAAC;gBACH,IAAI,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;oBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;;oBAC9E,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC;gBACP,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,EAAE,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC,CAAC;QACzC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE;YACrC,IAAI,MAAM,CAAC,MAAM,GAAG,SAAS;gBAAE,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE;YACrC,IAAI,MAAM,CAAC,MAAM,GAAG,IAAI;gBAAE,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1G,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CACzB,MAAM,CAAC;YACL,EAAE,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ;YAC3B,MAAM;YACN,IAAI;YACJ,QAAQ;YACR,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,IAAI,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;SAC9G,CAAC,CACH,CAAC;QACF,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACnC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAA8B,EAC9B,IAKC;IAED,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IACtC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1F,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,KAAK,IAAI,SAAS,EAAE,CAAC,CAAC;IAClF,OAAO,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACjG,CAAC;AAWD,uFAAuF;AACvF,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,OAAe;IACpE,OAAO,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,OAAO,OAAO,CAAC,CAAC;AACxD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,OAAe;IAClE,OAAO,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,SAAS,GAAG,gCAAgC,CAAC;AAEnD,mJAAmJ;AACnJ,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,OAAe,EAAE,OAA8B,EAAE,GAAqB,EAAE,KAAa,EAAE,GAAW;IAC/I,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO;IAC5B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,CAAC,IAAI,CAAC,kDAAkD,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACjG,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAoB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IACvG,eAAe,CAAC,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAC9F,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,OAAe;IACjE,MAAM,GAAG,GAAG,kBAAkB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,GAAG,CAAC;aACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;aAChC,IAAI,EAAE;aACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,OAAe;IAClE,OAAO,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC1D,MAAM,CAAC,GAAG,YAAY,CAAU,CAAC,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC,EAAE,CAAC,CAAC,CAAC;AACR,CAAC;AAsBD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAmD,EACnD,GAAqB,EACrB,IAAyB;IAEzB,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAC;IACjD,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,YAAY,CAAkB,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAC1D,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;YACV,SAAS;QACX,CAAC;QACD,IAAI,CAAC,CAAC,QAAQ,GAAG,YAAY,IAAI,KAAK,GAAG,CAAC,CAAC,aAAa,GAAG,kBAAkB;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC1G,CAAC;IACD,IAAI,OAAO;QAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,qDAAqD,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IAE5H,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,YAAY,CAAkB,CAAC,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,8CAA8C,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YACxF,MAAM,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3B,SAAS;QACX,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACf,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,GAAG;gBAAE,SAAS;YAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,aAAa,GAAG,IAAI,EAAE,aAAa,IAAI,CAAC,CAAC,aAAa,CAAC;YAC7D,IAAI,CAAC,CAAC,KAAK,GAAG,aAAa,GAAG,kBAAkB,CAAC;gBAAE,SAAS,CAAC,6CAA6C;YAC1G,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,IAAI,CAAC,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC9C,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,aAAa,EAAE,CAAC;QACnD,8GAA8G;QAC9G,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ;YAAE,MAAM;QACtE,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC;QAC9C,IAAI,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;QACvC,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,WAAW,CACxB,OAAO,CAAC,SAAS,CACf,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EACpE,GAAG,CACJ,EACD,IAAI,CAAC,cAAc,IAAI,wBAAwB,EAC/C,GAAG,OAAO,CAAC,IAAI,YAAY,CAC5B,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,yCAAyC,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAClJ,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAClC,2FAA2F;YAC3F,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC3H,IAAI,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;gBAChB,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;gBAClC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;gBAC3D,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACtB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBACtB,UAAU,EAAE,CAAC;gBACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC3B,CAAC;iBAAM,CAAC;gBACN,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,IAAI,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,YAAY,CAAC,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC;IAChG,IAAI,CAAC;QACH,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,kFAAkF;QAClF,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,yCAAyC,EAAE,EAAE,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvF,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IAC9C,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,MAAM,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const n = (v) => (Number.isFinite(v) && v > 0 ? Math.round(v) : 0);
|
|
2
|
+
const keyOf = (model, effort) => `${model}\u0000${effort ?? ''}`;
|
|
3
|
+
/**
|
|
4
|
+
* De-duplicates records per API response id (field-wise max, so a later, more complete copy wins)
|
|
5
|
+
* and sums per model × effort. Records without an id each count as their own response.
|
|
6
|
+
*/
|
|
7
|
+
export function sumUsage(records) {
|
|
8
|
+
const byResponse = new Map();
|
|
9
|
+
const anonymous = [];
|
|
10
|
+
for (const r of records) {
|
|
11
|
+
if (!r.model)
|
|
12
|
+
continue;
|
|
13
|
+
if (!r.responseId) {
|
|
14
|
+
anonymous.push(r);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const prev = byResponse.get(r.responseId);
|
|
18
|
+
if (!prev) {
|
|
19
|
+
byResponse.set(r.responseId, { ...r });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
prev.input = Math.max(n(prev.input), n(r.input));
|
|
23
|
+
prev.output = Math.max(n(prev.output), n(r.output));
|
|
24
|
+
prev.cacheRead = Math.max(n(prev.cacheRead), n(r.cacheRead));
|
|
25
|
+
prev.cacheWrite = Math.max(n(prev.cacheWrite), n(r.cacheWrite));
|
|
26
|
+
prev.reasoning = Math.max(n(prev.reasoning), n(r.reasoning));
|
|
27
|
+
}
|
|
28
|
+
const out = new Map();
|
|
29
|
+
for (const r of [...byResponse.values(), ...anonymous]) {
|
|
30
|
+
const effort = r.effort ?? null;
|
|
31
|
+
const k = keyOf(r.model, effort);
|
|
32
|
+
const e = out.get(k) ?? { model: r.model, effort, input: 0, output: 0, cache_read: 0, cache_write: 0, reasoning: 0, requests: 0 };
|
|
33
|
+
e.input += n(r.input);
|
|
34
|
+
e.output += n(r.output);
|
|
35
|
+
e.cache_read += n(r.cacheRead);
|
|
36
|
+
e.cache_write += n(r.cacheWrite);
|
|
37
|
+
e.reasoning += n(r.reasoning);
|
|
38
|
+
e.requests += 1;
|
|
39
|
+
out.set(k, e);
|
|
40
|
+
}
|
|
41
|
+
return [...out.values()];
|
|
42
|
+
}
|
|
43
|
+
/** Adds usage lists (e.g. a turn = main run + sub-agent runs), merging equal model × effort. */
|
|
44
|
+
export function mergeUsage(...lists) {
|
|
45
|
+
const out = new Map();
|
|
46
|
+
for (const list of lists) {
|
|
47
|
+
for (const u of list) {
|
|
48
|
+
const k = keyOf(u.model, u.effort);
|
|
49
|
+
const e = out.get(k) ?? { ...u, input: 0, output: 0, cache_read: 0, cache_write: 0, reasoning: 0, requests: 0 };
|
|
50
|
+
e.input += u.input;
|
|
51
|
+
e.output += u.output;
|
|
52
|
+
e.cache_read += u.cache_read;
|
|
53
|
+
e.cache_write += u.cache_write;
|
|
54
|
+
e.reasoning += u.reasoning;
|
|
55
|
+
e.requests += u.requests;
|
|
56
|
+
out.set(k, e);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...out.values()];
|
|
60
|
+
}
|
|
61
|
+
export const totalTokens = (u) => u.input + u.output + u.cache_read + u.cache_write;
|
|
62
|
+
/** Primary model × effort of a usage list: most output tokens, then most requests. */
|
|
63
|
+
export function primaryModel(usage) {
|
|
64
|
+
const best = [...usage].sort((a, b) => b.output - a.output || b.requests - a.requests)[0];
|
|
65
|
+
return best ? { model: best.model, effort: best.effort } : null;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=tokens.js.map
|