negotium 0.2.15 → 0.2.17
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/dist/agent-helpers.js +220 -32
- package/dist/agent-helpers.js.map +12 -12
- package/dist/background-bash.js.map +1 -1
- package/dist/browser-runtime.js.map +1 -1
- package/dist/{chunk-5g5a63vv.js → chunk-5cpdbbeh.js} +56 -6
- package/dist/{chunk-5g5a63vv.js.map → chunk-5cpdbbeh.js.map} +5 -5
- package/dist/hosted-agent.js +119 -9
- package/dist/hosted-agent.js.map +8 -8
- package/dist/main.js +931 -419
- package/dist/main.js.map +25 -24
- package/dist/mcp-factories.js +289 -42
- package/dist/mcp-factories.js.map +13 -13
- package/dist/prompts.js.map +1 -1
- package/dist/query-runtime.js.map +1 -1
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +2 -2
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/codex-provider.ts +53 -2
- package/dist/runtime/src/agents/compaction-support.ts +1 -1
- package/dist/runtime/src/agents/maestro-provider.ts +3 -0
- package/dist/runtime/src/agents/rollout/codex.ts +62 -0
- package/dist/runtime/src/index.ts +1 -0
- package/dist/runtime/src/mcp/factories/token-stats.ts +33 -11
- package/dist/runtime/src/mcp/runtime-spec.ts +58 -1
- package/dist/runtime/src/node-host.ts +1 -0
- package/dist/runtime/src/runtime/turn-event-stream.ts +43 -2
- package/dist/runtime/src/runtime/turn-runner.ts +20 -9
- package/dist/runtime/src/storage/token-stats.ts +200 -19
- package/dist/runtime/src/types.ts +6 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js.map +1 -1
- package/dist/storage.js +112 -11
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/agents/codex-provider.d.ts +13 -0
- package/dist/types/packages/core/src/agents/compaction-support.d.ts +1 -0
- package/dist/types/packages/core/src/agents/rollout/codex.d.ts +9 -0
- package/dist/types/packages/core/src/mcp/factories/token-stats.d.ts +4 -2
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +4 -0
- package/dist/types/packages/core/src/storage/token-stats.d.ts +35 -6
- package/dist/types/packages/core/src/types.d.ts +6 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js.map +1 -1
- package/package.json +2 -2
|
@@ -28,10 +28,12 @@ import {
|
|
|
28
28
|
hostedMcpServers,
|
|
29
29
|
} from "#agents/execution-host";
|
|
30
30
|
import {
|
|
31
|
+
type CodexTokenTotals,
|
|
31
32
|
migrateCodexRolloutNativeMultiAgentMetadata,
|
|
32
33
|
readCodexPatchCallIds,
|
|
33
34
|
readLatestCodexContextUsage,
|
|
34
35
|
readLatestCodexPatchPreview,
|
|
36
|
+
readLatestCodexTokenTotals,
|
|
35
37
|
} from "#agents/rollout/codex";
|
|
36
38
|
import { buildNumberedDiffSummary } from "#agents/tool-format";
|
|
37
39
|
import { extractFileEvents } from "#media/file-events";
|
|
@@ -44,6 +46,46 @@ import {
|
|
|
44
46
|
} from "#platform/mcp-config";
|
|
45
47
|
import type { AgentQueryOptions, EffortLevel, UnifiedEvent } from "#types";
|
|
46
48
|
|
|
49
|
+
type CodexEventUsage = {
|
|
50
|
+
input_tokens: number;
|
|
51
|
+
output_tokens: number;
|
|
52
|
+
cached_input_tokens?: number;
|
|
53
|
+
cache_write_input_tokens?: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function sameCodexUsage(usage: CodexEventUsage, total: CodexTokenTotals): boolean {
|
|
57
|
+
return (
|
|
58
|
+
usage.input_tokens === total.inputTokens &&
|
|
59
|
+
usage.output_tokens === total.outputTokens &&
|
|
60
|
+
(usage.cached_input_tokens ?? 0) === total.cachedInputTokens &&
|
|
61
|
+
(usage.cache_write_input_tokens ?? 0) === total.cacheWriteInputTokens
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Some Codex CLI builds expose the thread-lifetime counter on turn.completed
|
|
67
|
+
* despite the SDK declaring turn-local usage. Only subtract a rollout baseline
|
|
68
|
+
* when the event exactly matches the rollout's cumulative counter.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeCodexTurnUsage(
|
|
71
|
+
usage: CodexEventUsage,
|
|
72
|
+
baseline: CodexTokenTotals | undefined,
|
|
73
|
+
rolloutTotal: CodexTokenTotals | undefined,
|
|
74
|
+
): CodexEventUsage {
|
|
75
|
+
if (!baseline || !rolloutTotal || !sameCodexUsage(usage, rolloutTotal)) return usage;
|
|
76
|
+
const delta = (current: number, previous: number) =>
|
|
77
|
+
current >= previous ? current - previous : current;
|
|
78
|
+
return {
|
|
79
|
+
input_tokens: delta(rolloutTotal.inputTokens, baseline.inputTokens),
|
|
80
|
+
output_tokens: delta(rolloutTotal.outputTokens, baseline.outputTokens),
|
|
81
|
+
cached_input_tokens: delta(rolloutTotal.cachedInputTokens, baseline.cachedInputTokens),
|
|
82
|
+
cache_write_input_tokens: delta(
|
|
83
|
+
rolloutTotal.cacheWriteInputTokens,
|
|
84
|
+
baseline.cacheWriteInputTokens,
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
47
89
|
/**
|
|
48
90
|
* Pass Otium's EffortLevel through to Codex's ModelReasoningEffort.
|
|
49
91
|
*
|
|
@@ -696,6 +738,7 @@ export async function* codexProvider(opts: AgentQueryOptions): AsyncGenerator<Un
|
|
|
696
738
|
};
|
|
697
739
|
|
|
698
740
|
let currentSessionId = opts.sessionId;
|
|
741
|
+
let usageBaseline = currentSessionId ? readLatestCodexTokenTotals(currentSessionId) : undefined;
|
|
699
742
|
|
|
700
743
|
// Codex SDK has no systemPrompt option. A resumed thread may have been
|
|
701
744
|
// synthesized from a captured shell, so always refresh the current runtime
|
|
@@ -782,6 +825,7 @@ export async function* codexProvider(opts: AgentQueryOptions): AsyncGenerator<Un
|
|
|
782
825
|
thread = codex.startThread(threadOptions);
|
|
783
826
|
prompt = promptForThread(opts, true);
|
|
784
827
|
currentSessionId = undefined;
|
|
828
|
+
usageBaseline = undefined;
|
|
785
829
|
startResult = await startStreamedWithTracking(
|
|
786
830
|
thread,
|
|
787
831
|
prompt,
|
|
@@ -827,6 +871,7 @@ export async function* codexProvider(opts: AgentQueryOptions): AsyncGenerator<Un
|
|
|
827
871
|
input_tokens: number;
|
|
828
872
|
output_tokens: number;
|
|
829
873
|
cached_input_tokens?: number;
|
|
874
|
+
cache_write_input_tokens?: number;
|
|
830
875
|
};
|
|
831
876
|
error?: { message?: string };
|
|
832
877
|
};
|
|
@@ -931,9 +976,14 @@ export async function* codexProvider(opts: AgentQueryOptions): AsyncGenerator<Un
|
|
|
931
976
|
}
|
|
932
977
|
|
|
933
978
|
case "turn.completed": {
|
|
934
|
-
const
|
|
935
|
-
if (!
|
|
979
|
+
const rawUsage = event.usage;
|
|
980
|
+
if (!rawUsage) break;
|
|
936
981
|
attemptCompleted = true;
|
|
982
|
+
const usage = normalizeCodexTurnUsage(
|
|
983
|
+
rawUsage,
|
|
984
|
+
usageBaseline,
|
|
985
|
+
currentSessionId ? readLatestCodexTokenTotals(currentSessionId) : undefined,
|
|
986
|
+
);
|
|
937
987
|
const contextUsage = currentSessionId
|
|
938
988
|
? readLatestCodexContextUsage(currentSessionId)
|
|
939
989
|
: undefined;
|
|
@@ -944,6 +994,7 @@ export async function* codexProvider(opts: AgentQueryOptions): AsyncGenerator<Un
|
|
|
944
994
|
usage: {
|
|
945
995
|
inputTokens: usage.input_tokens,
|
|
946
996
|
outputTokens: usage.output_tokens,
|
|
997
|
+
cacheCreationInputTokens: usage.cache_write_input_tokens,
|
|
947
998
|
cacheReadInputTokens: usage.cached_input_tokens,
|
|
948
999
|
...contextUsage,
|
|
949
1000
|
},
|
|
@@ -54,7 +54,7 @@ function isCjkCodePoint(code: number): boolean {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function estimateTextTokens(text: string): number {
|
|
57
|
+
export function estimateTextTokens(text: string): number {
|
|
58
58
|
let cjkChars = 0;
|
|
59
59
|
let cjkUnits = 0;
|
|
60
60
|
for (let index = 0; index < text.length; index++) {
|
|
@@ -256,6 +256,9 @@ export function maestroProvider(opts: AgentQueryOptions): AsyncGenerator<Unified
|
|
|
256
256
|
...opts,
|
|
257
257
|
enableToolSearch: !opts.toolPolicy && opts.enableToolSearch !== false,
|
|
258
258
|
toolResultTruncation: buildMaestroToolResultTruncation(opts),
|
|
259
|
+
// Invocation-only runtime facts stay out of the stable system prompt and
|
|
260
|
+
// the SDK's persisted session projections.
|
|
261
|
+
ephemeralSystemPrompt: opts.ephemeralSystemPrompt,
|
|
259
262
|
// Resolve this after spreading caller options so untrusted runtime input
|
|
260
263
|
// cannot replace a topic owner's Vault credentials with another key.
|
|
261
264
|
apiKeyOverrides: resolveMaestroApiKeyOverrides(userId),
|
|
@@ -145,6 +145,13 @@ export interface CodexContextUsage {
|
|
|
145
145
|
contextWindow: number;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export interface CodexTokenTotals {
|
|
149
|
+
inputTokens: number;
|
|
150
|
+
outputTokens: number;
|
|
151
|
+
cachedInputTokens: number;
|
|
152
|
+
cacheWriteInputTokens: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
148
155
|
export interface CodexPatchChangePreview {
|
|
149
156
|
path: string;
|
|
150
157
|
before?: string;
|
|
@@ -389,6 +396,50 @@ export function extractLatestCodexContextUsage(jsonl: string): CodexContextUsage
|
|
|
389
396
|
return undefined;
|
|
390
397
|
}
|
|
391
398
|
|
|
399
|
+
/** Latest cumulative token counter persisted in a native Codex rollout. */
|
|
400
|
+
export function extractLatestCodexTokenTotals(jsonl: string): CodexTokenTotals | undefined {
|
|
401
|
+
const lines = jsonl.trimEnd().split("\n");
|
|
402
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
403
|
+
try {
|
|
404
|
+
const entry = JSON.parse(lines[index] ?? "") as {
|
|
405
|
+
type?: string;
|
|
406
|
+
payload?: {
|
|
407
|
+
type?: string;
|
|
408
|
+
info?: {
|
|
409
|
+
total_token_usage?: {
|
|
410
|
+
input_tokens?: number;
|
|
411
|
+
output_tokens?: number;
|
|
412
|
+
cached_input_tokens?: number;
|
|
413
|
+
cache_write_input_tokens?: number;
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
if (entry.type !== "event_msg" || entry.payload?.type !== "token_count") continue;
|
|
419
|
+
const usage = entry.payload.info?.total_token_usage;
|
|
420
|
+
if (!usage) continue;
|
|
421
|
+
const values = [
|
|
422
|
+
usage.input_tokens,
|
|
423
|
+
usage.output_tokens,
|
|
424
|
+
usage.cached_input_tokens,
|
|
425
|
+
usage.cache_write_input_tokens,
|
|
426
|
+
];
|
|
427
|
+
if (values.some((value) => value !== undefined && (!Number.isFinite(value) || value < 0))) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
432
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
433
|
+
cachedInputTokens: usage.cached_input_tokens ?? 0,
|
|
434
|
+
cacheWriteInputTokens: usage.cache_write_input_tokens ?? 0,
|
|
435
|
+
};
|
|
436
|
+
} catch {
|
|
437
|
+
// A concurrently appended final line may be incomplete; keep scanning.
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
392
443
|
/** Resolve a thread's current rollout and return its latest context measurement. */
|
|
393
444
|
export function readLatestCodexContextUsage(threadId: string): CodexContextUsage | undefined {
|
|
394
445
|
const path = latestCodexRolloutPath(threadId);
|
|
@@ -401,6 +452,17 @@ export function readLatestCodexContextUsage(threadId: string): CodexContextUsage
|
|
|
401
452
|
}
|
|
402
453
|
}
|
|
403
454
|
|
|
455
|
+
export function readLatestCodexTokenTotals(threadId: string): CodexTokenTotals | undefined {
|
|
456
|
+
const path = latestCodexRolloutPath(threadId);
|
|
457
|
+
if (!path) return undefined;
|
|
458
|
+
try {
|
|
459
|
+
return extractLatestCodexTokenTotals(readFileSync(path, "utf8"));
|
|
460
|
+
} catch (error) {
|
|
461
|
+
logger.debug({ error, threadId }, "codex token totals: rollout read failed");
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
404
466
|
/**
|
|
405
467
|
* Old and metadata-free Codex rollouts resume as native multi-agent v1 even
|
|
406
468
|
* when the feature flag is false. Rewrite their runtime metadata in place so
|
|
@@ -11,6 +11,7 @@ import "#platform/maestro-bootstrap-env";
|
|
|
11
11
|
// ── Agents ──────────────────────────────────────────────────────────
|
|
12
12
|
export { checkAgentAuth, checkAgentModelAuth } from "#agents/auth-check";
|
|
13
13
|
export { killOwnedCodexTreesForShutdown } from "#agents/codex-tree-kill";
|
|
14
|
+
export { estimateTextTokens } from "#agents/compaction-support";
|
|
14
15
|
export type { ForkHandle } from "#agents/fork";
|
|
15
16
|
export { cleanupAgentFork, forkAgentSession } from "#agents/fork";
|
|
16
17
|
export { runAgent, SUPPORTED_AGENTS } from "#agents/index";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { type Bucket, calcCost, getStats } from "#storage/token-stats";
|
|
3
|
+
import { type Bucket, type CurrentSessionUsage, calcCost, getStats } from "#storage/token-stats";
|
|
4
4
|
|
|
5
5
|
export interface TokenStatsMcpContext {
|
|
6
6
|
userId: string;
|
|
@@ -10,6 +10,8 @@ export interface TokenStatsSnapshot {
|
|
|
10
10
|
total: Bucket;
|
|
11
11
|
byHour: Record<string, Bucket>;
|
|
12
12
|
bySession: Record<string, Bucket>;
|
|
13
|
+
currentSessions?: CurrentSessionUsage[];
|
|
14
|
+
ignoredLegacyRecords?: number;
|
|
13
15
|
estimatedCostUsd: number;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -18,7 +20,11 @@ export interface TokenStatsMcpHost {
|
|
|
18
20
|
calcCost(
|
|
19
21
|
bucket: Pick<
|
|
20
22
|
Bucket,
|
|
21
|
-
|
|
23
|
+
| "inputTokens"
|
|
24
|
+
| "outputTokens"
|
|
25
|
+
| "cacheCreationInputTokens"
|
|
26
|
+
| "cacheReadInputTokens"
|
|
27
|
+
| "estimatedCostUsd"
|
|
22
28
|
>,
|
|
23
29
|
): number;
|
|
24
30
|
extraSummaryLines?(userId: string): readonly string[];
|
|
@@ -38,7 +44,7 @@ export function createTokenStatsMcpServer(
|
|
|
38
44
|
server.tool(
|
|
39
45
|
"get_usage_stats",
|
|
40
46
|
[
|
|
41
|
-
"Claude
|
|
47
|
+
"Claude/Codex/Maestro 사용량과 provider session의 최신 context 점유율을 조회합니다. 현재 실행 중인 turn은 완료 후 집계됩니다.",
|
|
42
48
|
"",
|
|
43
49
|
"from/to 생략 시 전체 기간. 자연어 시간 표현을 ISO 형식으로 변환해서 넘겨주세요.",
|
|
44
50
|
" 예: '지난 3시간' → from = 현재 -3h",
|
|
@@ -53,22 +59,38 @@ export function createTokenStatsMcpServer(
|
|
|
53
59
|
groupBy: z.enum(["hour", "session"]).optional().describe("집계 기준. 기본값: hour"),
|
|
54
60
|
},
|
|
55
61
|
async ({ from, to, groupBy = "hour" }) => {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const snapshot = host.getStats(context.userId, from, to);
|
|
63
|
+
const {
|
|
64
|
+
total,
|
|
65
|
+
byHour,
|
|
66
|
+
bySession,
|
|
67
|
+
currentSessions = [],
|
|
68
|
+
ignoredLegacyRecords = 0,
|
|
69
|
+
estimatedCostUsd,
|
|
70
|
+
} = snapshot;
|
|
61
71
|
const lines: string[] = [
|
|
62
|
-
"📊
|
|
72
|
+
"📊 Agent 사용량",
|
|
63
73
|
from || to ? `기간: ${from ?? "전체"} ~ ${to ?? "현재"}` : "기간: 전체",
|
|
64
74
|
"",
|
|
65
75
|
`쿼리 횟수: ${total.queries.toLocaleString()}회`,
|
|
66
|
-
|
|
76
|
+
`캐시 미적중 입력: ${total.inputTokens.toLocaleString()}`,
|
|
67
77
|
`출력 토큰: ${total.outputTokens.toLocaleString()}`,
|
|
68
78
|
`캐시 쓰기: ${total.cacheCreationInputTokens.toLocaleString()}`,
|
|
69
79
|
`캐시 읽기: ${total.cacheReadInputTokens.toLocaleString()}`,
|
|
70
80
|
`예상 비용: $${estimatedCostUsd.toFixed(4)} USD`,
|
|
71
81
|
];
|
|
82
|
+
if (ignoredLegacyRecords > 0) {
|
|
83
|
+
lines.push(`구형 레코드 제외: ${ignoredLegacyRecords.toLocaleString()}건`);
|
|
84
|
+
}
|
|
85
|
+
if (currentSessions.length > 0) {
|
|
86
|
+
lines.push("", `📐 현재 context (${currentSessions.length}개 provider session)`);
|
|
87
|
+
for (const current of currentSessions) {
|
|
88
|
+
const percent = (current.contextTokens / current.contextWindow) * 100;
|
|
89
|
+
lines.push(
|
|
90
|
+
` ${current.topicTitle} · ${current.agent}/${current.model} ${current.contextTokens.toLocaleString()} / ${current.contextWindow.toLocaleString()} (${percent.toFixed(1)}%)`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
72
94
|
const extraSummaryLines = host.extraSummaryLines?.(context.userId) ?? [];
|
|
73
95
|
if (extraSummaryLines.length > 0) lines.push("", ...extraSummaryLines);
|
|
74
96
|
|
|
@@ -79,7 +101,7 @@ export function createTokenStatsMcpServer(
|
|
|
79
101
|
lines.push("", `🗂 세션별 사용량 (${sorted.length}개)`);
|
|
80
102
|
for (const session of sorted) {
|
|
81
103
|
lines.push(
|
|
82
|
-
` ${session.name} 쿼리 ${session.queries}회 입력 ${session.inputTokens.toLocaleString()} / 출력 ${session.outputTokens.toLocaleString()} $${session.cost.toFixed(4)}`,
|
|
104
|
+
` ${session.name} 쿼리 ${session.queries}회 캐시 미적중 입력 ${session.inputTokens.toLocaleString()} / 캐시 읽기 ${session.cacheReadInputTokens.toLocaleString()} / 출력 ${session.outputTokens.toLocaleString()} $${session.cost.toFixed(4)}`,
|
|
83
105
|
);
|
|
84
106
|
}
|
|
85
107
|
} else {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `mcp/runtime-server.ts` (transport-agnostic parts only).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
11
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
12
12
|
import { NEGOTIUM_PORT, RUNTIME_MCP_SECRET } from "#platform/config";
|
|
13
13
|
import { type AgentKind, isAgentKind, type PeerRuntimeBridgeContext } from "#types";
|
|
14
14
|
|
|
@@ -233,9 +233,57 @@ export function buildRuntimeMcpSpec(
|
|
|
233
233
|
type: "sse" as const,
|
|
234
234
|
url: `${base}/sse?${query}`,
|
|
235
235
|
timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
|
|
236
|
+
...(agent === "maestro" ? { lifecycle: "turn" as const } : {}),
|
|
236
237
|
};
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
function hostedMcpCacheIdentity(surface: HostedMcpSurface, ctx: HostedMcpContext): string {
|
|
241
|
+
let semanticContext: unknown;
|
|
242
|
+
switch (surface) {
|
|
243
|
+
case "system-health":
|
|
244
|
+
semanticContext = {};
|
|
245
|
+
break;
|
|
246
|
+
case "token-stats":
|
|
247
|
+
case "agent-health":
|
|
248
|
+
semanticContext = { userId: ctx.userId };
|
|
249
|
+
break;
|
|
250
|
+
case "task":
|
|
251
|
+
semanticContext = {
|
|
252
|
+
userId: ctx.userId,
|
|
253
|
+
topicTitle: ctx.topicTitle,
|
|
254
|
+
topicId: ctx.topicId ?? null,
|
|
255
|
+
};
|
|
256
|
+
break;
|
|
257
|
+
case "wiki":
|
|
258
|
+
case "skills":
|
|
259
|
+
semanticContext = {
|
|
260
|
+
userId: ctx.userId,
|
|
261
|
+
topicId: ctx.wikiTopicId ?? ctx.topicId ?? null,
|
|
262
|
+
};
|
|
263
|
+
break;
|
|
264
|
+
case "vault":
|
|
265
|
+
semanticContext = { userId: ctx.userId, cwd: ctx.cwd, agent: ctx.agent };
|
|
266
|
+
break;
|
|
267
|
+
case "session-comm":
|
|
268
|
+
semanticContext = {
|
|
269
|
+
userId: ctx.userId,
|
|
270
|
+
topicTitle: ctx.topicTitle,
|
|
271
|
+
topicId: ctx.topicId ?? null,
|
|
272
|
+
subagentParentTopicId: ctx.subagentParentTopicId ?? null,
|
|
273
|
+
depth: ctx.depth ?? 0,
|
|
274
|
+
silent: ctx.silent ?? false,
|
|
275
|
+
agent: ctx.agent,
|
|
276
|
+
peerBridge: ctx.peerBridge ?? null,
|
|
277
|
+
};
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
const digest = createHash("sha256")
|
|
281
|
+
.update(JSON.stringify([surface, semanticContext]))
|
|
282
|
+
.digest("hex")
|
|
283
|
+
.slice(0, 24);
|
|
284
|
+
return `hosted:${surface}:${digest}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
239
287
|
/** Build an agent transport spec for one logical MCP surface on the shared runtime process. */
|
|
240
288
|
export function buildHostedMcpSpec(
|
|
241
289
|
agent: AgentKind,
|
|
@@ -246,9 +294,18 @@ export function buildHostedMcpSpec(
|
|
|
246
294
|
const base = `http://127.0.0.1:${runtimePort}${RUNTIME_MCP_BASE_PATH}/${surface}`;
|
|
247
295
|
const query = `token=${encodeURIComponent(token)}`;
|
|
248
296
|
if (agent === "codex") return { url: `${base}/mcp?${query}` };
|
|
297
|
+
const queryBound =
|
|
298
|
+
surface === "session-comm" && (ctx.silent === true || ctx.peerBridge !== undefined);
|
|
299
|
+
const lifecycle = queryBound ? "turn" : surface === "session-comm" ? "session" : "process";
|
|
249
300
|
return {
|
|
250
301
|
type: "sse" as const,
|
|
251
302
|
url: `${base}/sse?${query}`,
|
|
252
303
|
timeout: CLAUDE_MCP_TOOL_TIMEOUT_MS,
|
|
304
|
+
...(agent === "maestro"
|
|
305
|
+
? {
|
|
306
|
+
lifecycle,
|
|
307
|
+
cacheKey: hostedMcpCacheIdentity(surface, ctx),
|
|
308
|
+
}
|
|
309
|
+
: {}),
|
|
253
310
|
};
|
|
254
311
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { estimateTextTokens } from "#agents/compaction-support";
|
|
4
5
|
import { cleanupAgentFork } from "#agents/fork";
|
|
5
6
|
import { scheduleIdleArchiveForTopic } from "#agents/idle-archiver";
|
|
6
7
|
import { cancelPendingAskUserQuestions } from "#agents/mcp-tools/ask-user";
|
|
@@ -69,7 +70,13 @@ import {
|
|
|
69
70
|
markRuntimeUserTurnProviderSessionObserved,
|
|
70
71
|
} from "#storage/runtime-turn-requests";
|
|
71
72
|
import { recordUsage } from "#storage/token-stats";
|
|
72
|
-
import type {
|
|
73
|
+
import type {
|
|
74
|
+
AgentKind,
|
|
75
|
+
EffortLevel,
|
|
76
|
+
PeerRuntimeBridgeContext,
|
|
77
|
+
TokenUsage,
|
|
78
|
+
UnifiedEvent,
|
|
79
|
+
} from "#types";
|
|
73
80
|
import type { MessageDto } from "#types/api";
|
|
74
81
|
|
|
75
82
|
export type StreamAgentOutcome =
|
|
@@ -139,6 +146,9 @@ export async function runTurnEventStream(
|
|
|
139
146
|
let pendingSawDelta = false;
|
|
140
147
|
let accumulatedText = "";
|
|
141
148
|
let pendingText = "";
|
|
149
|
+
let assistantContextTokens = 0;
|
|
150
|
+
let toolContextTokens = 0;
|
|
151
|
+
let lastContextProgressAt = 0;
|
|
142
152
|
let lastVisibleMessageId: string | null = null;
|
|
143
153
|
const visibleMessageIds: string[] = [];
|
|
144
154
|
let lastTaskPanelText: string | null = null;
|
|
@@ -149,6 +159,26 @@ export async function runTurnEventStream(
|
|
|
149
159
|
const toolSummaryOptions = {
|
|
150
160
|
cwd: control.injectParams?.cwd ?? workspaceCwdFor(topicId),
|
|
151
161
|
};
|
|
162
|
+
const recordEventUsage = (usage: TokenUsage): void => {
|
|
163
|
+
recordUsage(userId, topicTitle, usage, {
|
|
164
|
+
topicId,
|
|
165
|
+
...(control.sessionId ? { providerSessionId: control.sessionId } : {}),
|
|
166
|
+
agent: agentType,
|
|
167
|
+
model,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const broadcastContextProgress = (force = false): void => {
|
|
171
|
+
if (silent) return;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
if (!force && lastContextProgressAt > 0 && now - lastContextProgressAt < 250) return;
|
|
174
|
+
lastContextProgressAt = now;
|
|
175
|
+
hub.broadcastAiStatus(topicId, {
|
|
176
|
+
kind: "context_progress",
|
|
177
|
+
queryId,
|
|
178
|
+
assistantTokens: assistantContextTokens,
|
|
179
|
+
toolTokens: toolContextTokens,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
152
182
|
const nextSyntheticToolUseId = () => `tool-${queryId}-${++syntheticToolCounter}`;
|
|
153
183
|
const bindToolUseId = (providerToolUseId?: string, fallback?: string): string => {
|
|
154
184
|
const providerId = normalizeToolUseId(providerToolUseId);
|
|
@@ -301,6 +331,8 @@ export async function runTurnEventStream(
|
|
|
301
331
|
const incoming = event.content;
|
|
302
332
|
accumulatedText += incoming;
|
|
303
333
|
pendingText += incoming;
|
|
334
|
+
assistantContextTokens = estimateTextTokens(accumulatedText);
|
|
335
|
+
broadcastContextProgress();
|
|
304
336
|
break;
|
|
305
337
|
}
|
|
306
338
|
case "text":
|
|
@@ -308,6 +340,8 @@ export async function runTurnEventStream(
|
|
|
308
340
|
const incoming = event.content;
|
|
309
341
|
accumulatedText += incoming;
|
|
310
342
|
pendingText += incoming;
|
|
343
|
+
assistantContextTokens = estimateTextTokens(accumulatedText);
|
|
344
|
+
broadcastContextProgress();
|
|
311
345
|
}
|
|
312
346
|
break;
|
|
313
347
|
case "tool_use":
|
|
@@ -316,6 +350,10 @@ export async function runTurnEventStream(
|
|
|
316
350
|
// original assistant → tool → assistant ordering instead of a block
|
|
317
351
|
// of tools followed by one consolidated answer at turn completion.
|
|
318
352
|
emitPendingAssistantMessage();
|
|
353
|
+
toolContextTokens += estimateTextTokens(
|
|
354
|
+
`${event.name}\n${JSON.stringify(event.input ?? {})}`,
|
|
355
|
+
);
|
|
356
|
+
broadcastContextProgress(true);
|
|
319
357
|
// Match both the bare name (Codex reports MCP server/tool separately)
|
|
320
358
|
// and each provider's public MCP-name form.
|
|
321
359
|
if (isVisualsShowHtmlTool(event.name)) {
|
|
@@ -529,6 +567,8 @@ export async function runTurnEventStream(
|
|
|
529
567
|
}
|
|
530
568
|
break;
|
|
531
569
|
case "tool_result":
|
|
570
|
+
toolContextTokens += estimateTextTokens(event.content);
|
|
571
|
+
broadcastContextProgress(true);
|
|
532
572
|
if (isVisualToolResultHandled(event.toolUseId)) break;
|
|
533
573
|
if (!silent) {
|
|
534
574
|
hub.broadcastToolOutput(
|
|
@@ -578,7 +618,7 @@ export async function runTurnEventStream(
|
|
|
578
618
|
}
|
|
579
619
|
break;
|
|
580
620
|
case "result":
|
|
581
|
-
if (event.usage)
|
|
621
|
+
if (event.usage) recordEventUsage(event.usage);
|
|
582
622
|
{
|
|
583
623
|
const usage: MessageDto["usage"] = event.usage
|
|
584
624
|
? {
|
|
@@ -648,6 +688,7 @@ export async function runTurnEventStream(
|
|
|
648
688
|
|
|
649
689
|
break;
|
|
650
690
|
case "error":
|
|
691
|
+
if (event.usage) recordEventUsage(event.usage);
|
|
651
692
|
logger.warn(
|
|
652
693
|
{ topicId, queryId, agentType, model, silent, error: event.content },
|
|
653
694
|
"ai: provider returned error",
|
|
@@ -139,6 +139,17 @@ export function withDefaultPlaywright(configuredMcp: string[], isManager: boolea
|
|
|
139
139
|
return [...enabled];
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
export function buildTurnReminderQuery(
|
|
143
|
+
agent: AgentKind,
|
|
144
|
+
prompt: string,
|
|
145
|
+
reminders: string[],
|
|
146
|
+
): { prompt: string; ephemeralSystemPrompt?: string } {
|
|
147
|
+
const reminderPrompt = reminders.join("\n\n");
|
|
148
|
+
if (!reminderPrompt) return { prompt };
|
|
149
|
+
if (agent === "maestro") return { prompt, ephemeralSystemPrompt: reminderPrompt };
|
|
150
|
+
return { prompt: `${prompt}\n\n${reminderPrompt}` };
|
|
151
|
+
}
|
|
152
|
+
|
|
142
153
|
// dequeueAll() is the merge primitive; this short gate makes the first reply
|
|
143
154
|
// to an idle caller wait long enough for sibling ask replies to join it.
|
|
144
155
|
const askReplyInjectBatcher = new DeferredInjectBatcher({
|
|
@@ -1424,8 +1435,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1424
1435
|
}
|
|
1425
1436
|
|
|
1426
1437
|
/**
|
|
1427
|
-
* Per-turn `<system-reminder>` blocks
|
|
1428
|
-
* than the system prompt.
|
|
1438
|
+
* Per-turn `<system-reminder>` blocks kept out of the stable system prompt.
|
|
1429
1439
|
*
|
|
1430
1440
|
* These facts flip between turns on the same model — whether the browser MCP
|
|
1431
1441
|
* came up, whether this turn is an auto-resume. A prompt cache invalidates
|
|
@@ -1433,8 +1443,9 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1433
1443
|
* every flip re-reads the entire prompt: measured against the live DeepSeek
|
|
1434
1444
|
* API, one flip at turn 15 of a 20-turn conversation cost 17,852 uncached
|
|
1435
1445
|
* tokens (hit=0) versus 1,215 with the reminder on the user turn — 40% more
|
|
1436
|
-
* uncached tokens across the run.
|
|
1437
|
-
*
|
|
1446
|
+
* uncached tokens across the run. Claude/Codex receive them at the user prompt
|
|
1447
|
+
* tail. Maestro projects them onto its invocation-start user wire message via
|
|
1448
|
+
* `ephemeralSystemPrompt`, fixed across that invocation's tool iterations.
|
|
1438
1449
|
*
|
|
1439
1450
|
* Applied after the conversation log is written, so a reminder never becomes
|
|
1440
1451
|
* part of the recorded user message (it would then persist into synthesized
|
|
@@ -1574,15 +1585,15 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1574
1585
|
"<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>",
|
|
1575
1586
|
);
|
|
1576
1587
|
}
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
|
|
1580
|
-
|
|
1588
|
+
// Build this after conversation recording: transient runtime facts must be
|
|
1589
|
+
// supplied again by the host on a later external turn, never resumed from
|
|
1590
|
+
// Negotium's canonical conversation state.
|
|
1591
|
+
const reminderQuery = buildTurnReminderQuery(agentKind, agentPrompt, turnReminders);
|
|
1581
1592
|
|
|
1582
1593
|
try {
|
|
1583
1594
|
yield* runAgent({
|
|
1584
1595
|
agent: agentKind,
|
|
1585
|
-
|
|
1596
|
+
...reminderQuery,
|
|
1586
1597
|
attachments: promptAttachments,
|
|
1587
1598
|
cwd: workspaceCwd,
|
|
1588
1599
|
systemPrompt,
|