@zq-silk/yui 2.3.0 → 2.4.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 +15 -1
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +17 -8
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +25 -2
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +10 -1
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +25 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +13 -5
- package/dist/integration/gitIntegrationService.js +7 -12
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +48 -71
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +2 -1
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +295 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +106 -8
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
- package/docs/release-workflow.md +28 -0
- package/docs/release-workflow.zh-CN.md +21 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/dist/web/assets/client/app.js +0 -808
- package/dist/web/assets/client/components.js +0 -477
- package/dist/web/assets/client/detail.js +0 -379
- package/dist/web/assets/client/dock.js +0 -309
- package/dist/web/assets/client/evidence.js +0 -364
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1277
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -198
- package/dist/web/assets/client/sections.js +0 -349
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -216
- package/dist/web/assets/styles/views.js +0 -309
- /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
|
|
3
|
+
import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
|
|
4
|
+
const READS = new Set([
|
|
5
|
+
"config/read", "configRequirements/read", "model/list", "collaborationMode/list",
|
|
6
|
+
"experimentalFeature/list", "skills/list", "hooks/list", "mcpServerStatus/list", "account/read",
|
|
7
|
+
"account/rateLimits/read", "modelProvider/capabilities/read", "permissionProfile/list",
|
|
8
|
+
"thread/read", "thread/turns/list", "thread/items/list", "thread/goal/get"
|
|
9
|
+
]);
|
|
10
|
+
const REQUESTS = new Set([
|
|
11
|
+
"item/commandExecution/requestApproval", "item/fileChange/requestApproval",
|
|
12
|
+
"item/tool/requestUserInput", "item/permissions/requestApproval", "mcpServer/elicitation/request"
|
|
13
|
+
]);
|
|
14
|
+
/** A view of one existing connection, never another Thread or durable input queue.
|
|
15
|
+
* Both the native TUI and Web answer the original server request on this transport. */
|
|
16
|
+
export function createCodexNativeAccess(threadId, initialized, channel, openingMessages = [], initialConfiguration) {
|
|
17
|
+
const pending = new Map();
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
let configuration = initialConfiguration;
|
|
20
|
+
let observedAt = new Date().toISOString();
|
|
21
|
+
const observed = () => typeof configuration?.model !== "string"
|
|
22
|
+
? unknownAgentRunConfiguration("No confirmed native model response. Reconnect to restore the fixed launch configuration.")
|
|
23
|
+
: {
|
|
24
|
+
status: "observed", observedAt,
|
|
25
|
+
handshake: { status: "unsupported", reason: "Model reported by Codex thread/start or thread/resume; not a capability handshake." },
|
|
26
|
+
requested: [],
|
|
27
|
+
axes: [{ key: "model", category: "model", current: { status: "observed", value: configuration.model }, offered: [] }]
|
|
28
|
+
};
|
|
29
|
+
const observe = (message) => {
|
|
30
|
+
const params = message.params;
|
|
31
|
+
if (params?.threadId !== threadId)
|
|
32
|
+
return;
|
|
33
|
+
if ((typeof message.id === "string" || typeof message.id === "number")
|
|
34
|
+
&& typeof message.method === "string" && REQUESTS.has(message.method)
|
|
35
|
+
&& (typeof params.turnId === "string"
|
|
36
|
+
|| (message.method === "mcpServer/elicitation/request" && params.turnId == null))) {
|
|
37
|
+
pending.set(message.id, { id: message.id, method: message.method,
|
|
38
|
+
threadId, turnId: typeof params.turnId === "string" ? params.turnId : null, params });
|
|
39
|
+
}
|
|
40
|
+
if (message.method === "serverRequest/resolved")
|
|
41
|
+
pending.delete(params.requestId);
|
|
42
|
+
if (message.method === "turn/completed") {
|
|
43
|
+
const turn = params.turn;
|
|
44
|
+
for (const [id, request] of pending)
|
|
45
|
+
if (request.turnId === turn?.id)
|
|
46
|
+
pending.delete(id);
|
|
47
|
+
}
|
|
48
|
+
for (const listener of listeners)
|
|
49
|
+
listener(message);
|
|
50
|
+
};
|
|
51
|
+
for (const message of openingMessages)
|
|
52
|
+
observe(message);
|
|
53
|
+
channel.onMessage(observe);
|
|
54
|
+
return {
|
|
55
|
+
initialized,
|
|
56
|
+
get attached() { return listeners.size > 0; },
|
|
57
|
+
get requests() { return [...pending.values()]; },
|
|
58
|
+
get runConfiguration() { return observed(); },
|
|
59
|
+
async setModel(model) {
|
|
60
|
+
try {
|
|
61
|
+
if (!model.trim() || model.length > 200 || model.includes("\0"))
|
|
62
|
+
throw new Error("Invalid model.");
|
|
63
|
+
const read = await channel.request("thread/read", { threadId, includeTurns: false });
|
|
64
|
+
const thread = read.thread;
|
|
65
|
+
if (thread?.id !== threadId || thread.status?.type !== "idle") {
|
|
66
|
+
throw new Error("Model switching requires a confirmed idle Thread.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new CodexPreSubmissionError(error instanceof Error ? error.message : "Native idle state is unavailable.");
|
|
71
|
+
}
|
|
72
|
+
// No fork/start, account, permission or Role-default mutation. Do not
|
|
73
|
+
// preserve an old observation if the native mutation becomes uncertain.
|
|
74
|
+
configuration = undefined;
|
|
75
|
+
const result = await channel.request("thread/resume", { threadId, model, excludeTurns: true });
|
|
76
|
+
if (result.thread?.id !== threadId || result.model !== model) {
|
|
77
|
+
throw new Error("Native model adoption was not confirmed; inspect before another change.");
|
|
78
|
+
}
|
|
79
|
+
configuration = result;
|
|
80
|
+
observedAt = new Date().toISOString();
|
|
81
|
+
},
|
|
82
|
+
async read(method, params) {
|
|
83
|
+
// TUI startup attaches to the already opened Thread without resuming it
|
|
84
|
+
// with new options or changing the Host's configured execution boundary.
|
|
85
|
+
if (method === "thread/resume") {
|
|
86
|
+
if (params.threadId !== threadId)
|
|
87
|
+
throw new Error("Select the existing Session; new/forked Threads are not supported here.");
|
|
88
|
+
const page = params.initialTurnsPage;
|
|
89
|
+
configuration = await channel.request("thread/resume", { threadId,
|
|
90
|
+
...(typeof params.excludeTurns === "boolean" ? { excludeTurns: params.excludeTurns } : {}),
|
|
91
|
+
...(page ? { initialTurnsPage: { ...page, limit: Math.min(40, typeof page.limit === "number" && page.limit > 0 ? page.limit : 40) } } : {}) });
|
|
92
|
+
observedAt = new Date().toISOString();
|
|
93
|
+
return configuration;
|
|
94
|
+
}
|
|
95
|
+
if (!READS.has(method))
|
|
96
|
+
throw new Error(`Native operation is not available in managed access: ${method}.`);
|
|
97
|
+
if (method.startsWith("thread/") && params.threadId !== threadId) {
|
|
98
|
+
throw new Error("Native read targets a different Thread.");
|
|
99
|
+
}
|
|
100
|
+
// Native TUI owns its hydration protocol (legacy TUI can explicitly ask
|
|
101
|
+
// for full turns). Web history remains independently bounded/paginated.
|
|
102
|
+
return channel.request(method, method === "thread/turns/list" || method === "thread/items/list"
|
|
103
|
+
? { ...params, limit: Math.min(40, typeof params.limit === "number" && params.limit > 0 ? params.limit : 40) }
|
|
104
|
+
: params);
|
|
105
|
+
},
|
|
106
|
+
validateTurn(params) {
|
|
107
|
+
if (!configuration)
|
|
108
|
+
throw new Error("Native TUI has not attached to the existing Thread.");
|
|
109
|
+
for (const [key, source] of Object.entries({
|
|
110
|
+
model: "model", cwd: "cwd", runtimeWorkspaceRoots: "runtimeWorkspaceRoots",
|
|
111
|
+
approvalPolicy: "approvalPolicy", approvalsReviewer: "approvalsReviewer",
|
|
112
|
+
effort: "reasoningEffort", collaborationMode: "collaborationMode"
|
|
113
|
+
})) {
|
|
114
|
+
if (params[key] != null && !isDeepStrictEqual(params[key], configuration[source])) {
|
|
115
|
+
throw new Error(`Native ${key} differs from the managed Session; change it through Yui configuration.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const key of ["sandboxPolicy", "permissions", "environments", "toolOutput", "additionalContext", "outputSchema", "disabledPluginIds", "serviceTierForTurn", "summary", "personality", "cyberAccessProgram"]) {
|
|
119
|
+
if (params[key] != null)
|
|
120
|
+
throw new Error(`Native ${key} overrides are not supported in managed access.`);
|
|
121
|
+
}
|
|
122
|
+
if (params.serviceTier != null && params.serviceTier !== (configuration.serviceTier ?? "default")) {
|
|
123
|
+
throw new Error("Native service tier differs from the managed Session.");
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
async respond(id, expectedThread, turnId, result) {
|
|
127
|
+
const request = pending.get(id);
|
|
128
|
+
if (!request || request.threadId !== expectedThread || request.turnId !== turnId) {
|
|
129
|
+
throw new Error("Native request is no longer pending for this exact Thread and Turn.");
|
|
130
|
+
}
|
|
131
|
+
validateNativeResponse(request, result);
|
|
132
|
+
// Claim before awaiting transport. A lost send acknowledgement is unknown,
|
|
133
|
+
// not permission for another entry to repeat a possibly applied answer.
|
|
134
|
+
pending.delete(id);
|
|
135
|
+
await channel.send({ id, result });
|
|
136
|
+
},
|
|
137
|
+
events(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; }
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function validateNativeResponse(request, result) {
|
|
141
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
142
|
+
throw new Error("Missing native response.");
|
|
143
|
+
if (request.method === "mcpServer/elicitation/request") {
|
|
144
|
+
if (!["accept", "decline", "cancel"].includes(String(result.action))
|
|
145
|
+
|| (result.action !== "accept" && result.content != null)) {
|
|
146
|
+
throw new Error("Invalid native MCP elicitation response.");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (request.method === "item/tool/requestUserInput") {
|
|
150
|
+
const answers = result.answers;
|
|
151
|
+
const questions = request.params.questions;
|
|
152
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers) || !Array.isArray(questions)) {
|
|
153
|
+
throw new Error("Native question requires answers keyed by question id.");
|
|
154
|
+
}
|
|
155
|
+
const ids = questions.map(q => q.id);
|
|
156
|
+
if (Object.keys(answers).some(id => !ids.includes(id))
|
|
157
|
+
|| ids.some(id => {
|
|
158
|
+
if (typeof id !== "string")
|
|
159
|
+
return true;
|
|
160
|
+
const answer = answers[id];
|
|
161
|
+
return !Array.isArray(answer?.answers) || answer.answers.some(value => typeof value !== "string");
|
|
162
|
+
})) {
|
|
163
|
+
throw new Error("Native answers do not match the original questions.");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (request.method === "item/permissions/requestApproval") {
|
|
167
|
+
// Web currently exposes decline only; richer native grants remain the TUI's
|
|
168
|
+
// protocol responsibility. Never infer broad grants from an accept button.
|
|
169
|
+
if (!result.permissions || typeof result.permissions !== "object")
|
|
170
|
+
throw new Error("Missing native permissions response.");
|
|
171
|
+
}
|
|
172
|
+
else if (!["accept", "acceptForSession", "decline", "cancel"].includes(String(result.decision))) {
|
|
173
|
+
throw new Error("Invalid native approval decision.");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
5
|
+
import { localCodexTuiEnvironment } from "./codexInteractiveHost.js";
|
|
6
|
+
/** The TUI is a view of the Host-owned connection. It cannot launch, fork,
|
|
7
|
+
* reconfigure, or submit outside the Host's existing input admission. */
|
|
8
|
+
export async function openControlledCodexTui(payload, threadId, access, input) {
|
|
9
|
+
const baseArgs = JSON.parse(payload.environment.YUI_AGENT_BASE_ARGS ?? "[]");
|
|
10
|
+
if (!Array.isArray(baseArgs) || baseArgs.some(arg => typeof arg !== "string"))
|
|
11
|
+
throw new Error("Invalid Codex base arguments.");
|
|
12
|
+
const token = randomBytes(32).toString("hex");
|
|
13
|
+
const server = new WebSocketServer({
|
|
14
|
+
host: "127.0.0.1", port: 0, maxPayload: 1024 * 1024, perMessageDeflate: false,
|
|
15
|
+
verifyClient: (info) => info.req.headers.authorization === `Bearer ${token}`
|
|
16
|
+
});
|
|
17
|
+
let socket;
|
|
18
|
+
let closed = false;
|
|
19
|
+
let markReady;
|
|
20
|
+
let failReady;
|
|
21
|
+
const ready = new Promise((resolve, reject) => { markReady = resolve; failReady = reject; });
|
|
22
|
+
void ready.catch(() => { });
|
|
23
|
+
const unsubscribe = access.events(message => {
|
|
24
|
+
if (socket?.readyState === WebSocket.OPEN)
|
|
25
|
+
socket.send(JSON.stringify(message));
|
|
26
|
+
});
|
|
27
|
+
server.on("connection", client => {
|
|
28
|
+
if (socket || closed) {
|
|
29
|
+
client.close(1008, "TUI already attached.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
socket = client;
|
|
33
|
+
client.on("error", input.onError);
|
|
34
|
+
client.on("message", (bytes, binary) => {
|
|
35
|
+
void (async () => {
|
|
36
|
+
if (binary)
|
|
37
|
+
throw new Error("Expected native JSON request.");
|
|
38
|
+
const message = JSON.parse(bytes.toString());
|
|
39
|
+
const id = message.id;
|
|
40
|
+
if (typeof id !== "number" && typeof id !== "string")
|
|
41
|
+
return; // notifications
|
|
42
|
+
try {
|
|
43
|
+
if (message.method === undefined) {
|
|
44
|
+
const request = access.requests.find(request => request.id === id);
|
|
45
|
+
if (!request)
|
|
46
|
+
throw new Error("Native request is no longer pending.");
|
|
47
|
+
await input.respond(id, request.turnId, message.result);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const method = String(message.method), params = (message.params ?? {});
|
|
51
|
+
if (method === "turn/start")
|
|
52
|
+
access.validateTurn(params);
|
|
53
|
+
const result = method === "initialize" ? access.initialized
|
|
54
|
+
: ["turn/start", "turn/steer", "turn/interrupt"].includes(method) ? await input.mutate(method, params)
|
|
55
|
+
: await access.read(method, params);
|
|
56
|
+
if (client.readyState === WebSocket.OPEN)
|
|
57
|
+
client.send(JSON.stringify({ id, result }));
|
|
58
|
+
if (method === "thread/resume") {
|
|
59
|
+
markReady();
|
|
60
|
+
for (const request of access.requests)
|
|
61
|
+
client.send(JSON.stringify({
|
|
62
|
+
id: request.id, method: request.method, params: request.params
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (client.readyState === WebSocket.OPEN)
|
|
68
|
+
client.send(JSON.stringify({
|
|
69
|
+
id, error: { code: -32600, message: error instanceof Error ? error.message : String(error) }
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
})().catch(input.onError);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
await once(server, "listening");
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
unsubscribe();
|
|
80
|
+
server.close();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const address = server.address();
|
|
84
|
+
if (!address || typeof address === "string")
|
|
85
|
+
throw new Error("Native TUI relay did not bind.");
|
|
86
|
+
const child = spawn(payload.command, [...baseArgs,
|
|
87
|
+
"--remote", `ws://127.0.0.1:${address.port}`, "--remote-auth-token-env", "YUI_CODEX_REMOTE_AUTH_TOKEN",
|
|
88
|
+
"resume", threadId], {
|
|
89
|
+
cwd: payload.cwd, stdio: "inherit",
|
|
90
|
+
env: { ...localCodexTuiEnvironment(payload.environment), YUI_CODEX_REMOTE_AUTH_TOKEN: token }
|
|
91
|
+
});
|
|
92
|
+
const close = () => {
|
|
93
|
+
if (closed)
|
|
94
|
+
return;
|
|
95
|
+
closed = true;
|
|
96
|
+
unsubscribe();
|
|
97
|
+
socket?.terminate();
|
|
98
|
+
server.close();
|
|
99
|
+
child.kill("SIGTERM");
|
|
100
|
+
};
|
|
101
|
+
server.on("error", input.onError);
|
|
102
|
+
child.on("error", error => { failReady(error); input.onError(error); close(); input.onExit(); });
|
|
103
|
+
child.once("exit", () => { failReady(new Error("Native TUI exited before attachment.")); close(); input.onExit(); });
|
|
104
|
+
const deadline = setTimeout(() => failReady(new Error("Native TUI did not attach to the selected Thread.")), 10_000);
|
|
105
|
+
try {
|
|
106
|
+
await ready;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
close();
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
clearTimeout(deadline);
|
|
114
|
+
}
|
|
115
|
+
return { close };
|
|
116
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function foldPublicReply(previous, nativeSessionId, method, params) {
|
|
2
|
+
if (params.threadId !== nativeSessionId)
|
|
3
|
+
return previous;
|
|
4
|
+
if (method === "turn/started")
|
|
5
|
+
return undefined;
|
|
6
|
+
if (typeof params.turnId !== "string")
|
|
7
|
+
return previous;
|
|
8
|
+
if (method === "item/agentMessage/delta" && typeof params.itemId === "string" && typeof params.delta === "string") {
|
|
9
|
+
const same = previous?.id === params.itemId && previous.turnId === params.turnId;
|
|
10
|
+
const text = (same ? previous.text : "") + params.delta;
|
|
11
|
+
return { nativeSessionId, turnId: params.turnId, id: params.itemId,
|
|
12
|
+
text: text.slice(0, 6000), truncated: text.length > 6000 || !!(same && previous.truncated) };
|
|
13
|
+
}
|
|
14
|
+
if ((method === "item/started" || method === "item/completed")
|
|
15
|
+
&& params.item !== null && typeof params.item === "object") {
|
|
16
|
+
const item = params.item;
|
|
17
|
+
if (item.type === "agentMessage" && typeof item.id === "string" && typeof item.text === "string") {
|
|
18
|
+
return { nativeSessionId, turnId: params.turnId, id: item.id,
|
|
19
|
+
text: item.text.slice(0, 6000), truncated: item.text.length > 6000 };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return previous;
|
|
23
|
+
}
|
|
@@ -15,6 +15,11 @@ export function isRuntimeTokenEvidence(observation) {
|
|
|
15
15
|
&& (observation.payload.observationQuality === "partial"
|
|
16
16
|
|| observation.payload.observationQuality === "unavailable")));
|
|
17
17
|
}
|
|
18
|
+
/** Both money and tokens survive activity compaction; only tokens feed token metrics. */
|
|
19
|
+
export function isRuntimeUsageEvidence(observation) {
|
|
20
|
+
return isRuntimeTokenEvidence(observation)
|
|
21
|
+
|| (observation.kind === "activity.observed" && observation.payload.cost !== undefined);
|
|
22
|
+
}
|
|
18
23
|
const KINDS = [
|
|
19
24
|
"host.observed",
|
|
20
25
|
"session.started",
|
|
@@ -296,6 +301,11 @@ function normalizePayload(kind, input) {
|
|
|
296
301
|
const observerSource = input.observerSource === undefined
|
|
297
302
|
? undefined
|
|
298
303
|
: normalizeObserverSource(input.observerSource);
|
|
304
|
+
const cost = input.cost === undefined ? undefined : normalizeCost(input.cost);
|
|
305
|
+
if (cost !== undefined && (kind !== "activity.observed"
|
|
306
|
+
|| (cost.semantics === "request" && input.activityId === undefined))) {
|
|
307
|
+
throw new Error("Cost requires activity.observed and a stable request activityId.");
|
|
308
|
+
}
|
|
299
309
|
if (kind === "turn.failed" && input.failure === undefined) {
|
|
300
310
|
throw new Error("run.failed requires normalized failure evidence.");
|
|
301
311
|
}
|
|
@@ -376,6 +386,7 @@ function normalizePayload(kind, input) {
|
|
|
376
386
|
...input.usage,
|
|
377
387
|
semantics: input.usage.semantics ?? "cumulative-session"
|
|
378
388
|
}) }),
|
|
389
|
+
...(cost === undefined ? {} : { cost }),
|
|
379
390
|
...(observerSource === undefined ? {} : { observerSource }),
|
|
380
391
|
...(input.sourceId === undefined
|
|
381
392
|
? {}
|
|
@@ -434,6 +445,43 @@ function normalizePayload(kind, input) {
|
|
|
434
445
|
...(input.goalTokenBudget === undefined ? {} : { goalTokenBudget: input.goalTokenBudget })
|
|
435
446
|
});
|
|
436
447
|
}
|
|
448
|
+
function normalizeCost(input) {
|
|
449
|
+
if (input === null || typeof input !== "object"
|
|
450
|
+
|| !["actual", "estimated"].includes(input.kind)
|
|
451
|
+
|| !["request", "cumulative-session"].includes(input.semantics)) {
|
|
452
|
+
throw new Error("Runtime cost kind or semantics is invalid.");
|
|
453
|
+
}
|
|
454
|
+
if (!Number.isFinite(input.amount) || input.amount < 0 || input.amount > Number.MAX_SAFE_INTEGER) {
|
|
455
|
+
throw new Error("Runtime cost amount must be finite and non-negative.");
|
|
456
|
+
}
|
|
457
|
+
if (typeof input.currency !== "string" || !/^[A-Z]{3}$/u.test(input.currency)) {
|
|
458
|
+
throw new Error("Runtime cost currency requires a three-letter currency code.");
|
|
459
|
+
}
|
|
460
|
+
if (input.kind === "estimated" && input.basis === undefined) {
|
|
461
|
+
throw new Error("Estimated cost requires a price basis.");
|
|
462
|
+
}
|
|
463
|
+
const basis = input.basis;
|
|
464
|
+
if (basis !== undefined && (!Array.isArray(basis.excluded) || basis.excluded.length > 16)) {
|
|
465
|
+
throw new Error("Runtime cost basis excluded items must be a bounded list.");
|
|
466
|
+
}
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
kind: input.kind, semantics: input.semantics, amount: input.amount,
|
|
469
|
+
currency: input.currency, source: costText(input.source, "Cost source"),
|
|
470
|
+
...(basis === undefined ? {} : { basis: Object.freeze({
|
|
471
|
+
model: costText(basis.model, "Cost model"),
|
|
472
|
+
source: costText(basis.source, "Price source"),
|
|
473
|
+
version: costText(basis.version, "Price version or date"),
|
|
474
|
+
scope: costText(basis.scope, "Estimate scope"),
|
|
475
|
+
excluded: Object.freeze(basis.excluded.map(value => costText(value, "Excluded cost", 128)))
|
|
476
|
+
}) })
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function costText(value, label, limit = 512) {
|
|
480
|
+
const text = requireText(value, label);
|
|
481
|
+
if (text.length > limit)
|
|
482
|
+
throw new Error(`${label} exceeds ${limit} characters.`);
|
|
483
|
+
return text;
|
|
484
|
+
}
|
|
437
485
|
export function runtimeObservationSemanticKey(input) {
|
|
438
486
|
const fence = input.fence;
|
|
439
487
|
const continuationIdentity = [
|
|
@@ -13,6 +13,8 @@ import { unsupportedAgentRunConfiguration } from "./agentRunConfiguration.js";
|
|
|
13
13
|
import { codexAppServerErrorIsMissing, CodexAppServerRequestError, CodexAppServerRuntime, codexClientInitialization, codexGoalNotification, codexTurnInput, codexTurnOutput } from "./codexAppServerRuntime.js";
|
|
14
14
|
import { JsonLineChannel, PROVIDER_MESSAGE_MAX_BYTES, terminateProcessGroup } from "./jsonLineChannel.js";
|
|
15
15
|
import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
16
|
+
import { foldPublicReply } from "./publicReply.js";
|
|
17
|
+
import { createCodexNativeAccess } from "./codexNativeAccess.js";
|
|
16
18
|
const CODEX_PROXY_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
17
19
|
/**
|
|
18
20
|
* These Session implementations do not expose run-configuration observations.
|
|
@@ -297,7 +299,7 @@ class CodexProxyWebSocketChannel {
|
|
|
297
299
|
return;
|
|
298
300
|
}
|
|
299
301
|
const id = requestId(message.id);
|
|
300
|
-
const pending = id === undefined ? undefined : this.#pending.get(id);
|
|
302
|
+
const pending = id === undefined || message.method !== undefined ? undefined : this.#pending.get(id);
|
|
301
303
|
if (pending !== undefined) {
|
|
302
304
|
clearTimeout(pending.timer);
|
|
303
305
|
this.#pending.delete(id);
|
|
@@ -382,6 +384,7 @@ class CodexStructuredProviderSession {
|
|
|
382
384
|
onActivity;
|
|
383
385
|
mirror;
|
|
384
386
|
nativeAccountHome;
|
|
387
|
+
nativeAccess;
|
|
385
388
|
#turnAttempts = new Map();
|
|
386
389
|
adapterId = "codex";
|
|
387
390
|
#activeTurnId;
|
|
@@ -391,6 +394,7 @@ class CodexStructuredProviderSession {
|
|
|
391
394
|
#bufferedStarts = [];
|
|
392
395
|
#bufferedTerminals = [];
|
|
393
396
|
#bufferedActivities = [];
|
|
397
|
+
publicReply;
|
|
394
398
|
constructor(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, nativeAccountHome) {
|
|
395
399
|
this.child = child;
|
|
396
400
|
this.exit = exit;
|
|
@@ -412,13 +416,16 @@ class CodexStructuredProviderSession {
|
|
|
412
416
|
await channel.notify("initialized");
|
|
413
417
|
const runtime = new CodexAppServerRuntime(channel);
|
|
414
418
|
let conversationId;
|
|
419
|
+
let nativeConfiguration;
|
|
415
420
|
let resumedActiveTurnId;
|
|
416
421
|
let resumedTurns = [];
|
|
417
422
|
if (control.mode === "new") {
|
|
418
|
-
|
|
423
|
+
const opened = await runtime.openConversation({
|
|
419
424
|
cwd: payload.cwd,
|
|
420
425
|
...control.codexThread
|
|
421
|
-
})
|
|
426
|
+
});
|
|
427
|
+
conversationId = opened.conversationId;
|
|
428
|
+
nativeConfiguration = opened.raw;
|
|
422
429
|
}
|
|
423
430
|
else {
|
|
424
431
|
try {
|
|
@@ -427,6 +434,7 @@ class CodexStructuredProviderSession {
|
|
|
427
434
|
...control.codexThread
|
|
428
435
|
});
|
|
429
436
|
conversationId = resumed.threadId;
|
|
437
|
+
nativeConfiguration = resumed.raw;
|
|
430
438
|
resumedActiveTurnId = resumed.activeTurnId;
|
|
431
439
|
resumedTurns = resumed.turns;
|
|
432
440
|
}
|
|
@@ -456,6 +464,7 @@ class CodexStructuredProviderSession {
|
|
|
456
464
|
}
|
|
457
465
|
}
|
|
458
466
|
const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, optionalId(initialized.codexHome));
|
|
467
|
+
session.nativeAccess = createCodexNativeAccess(conversationId, initialized, channel, openingMessages, nativeConfiguration);
|
|
459
468
|
session.#activeTurnId = resumedActiveTurnId;
|
|
460
469
|
const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
|
|
461
470
|
session.#clientOwnedTurnId = ownedTurn?.turnId;
|
|
@@ -514,6 +523,8 @@ class CodexStructuredProviderSession {
|
|
|
514
523
|
const method = typeof message.method === "string" ? message.method : "";
|
|
515
524
|
const params = object(message.params) ?? {};
|
|
516
525
|
const threadId = optionalId(params.threadId);
|
|
526
|
+
if (emit)
|
|
527
|
+
this.publicReply = foldPublicReply(this.publicReply, this.conversationId, method, params);
|
|
517
528
|
if (threadId !== this.conversationId) {
|
|
518
529
|
if (threadId === undefined && (method === "turn/started" || method === "turn/completed")) {
|
|
519
530
|
this.mirror("stderr", `Codex protocol: ${method} has no thread identity; ignored.\n`);
|
|
@@ -630,11 +641,9 @@ class CodexStructuredProviderSession {
|
|
|
630
641
|
get activeTurnId() {
|
|
631
642
|
return this.#activeTurnId;
|
|
632
643
|
}
|
|
633
|
-
/**
|
|
634
|
-
* This implementation has no run-configuration observation reader.
|
|
635
|
-
*/
|
|
644
|
+
/** Last model confirmation from native start/resume, with its observation time. */
|
|
636
645
|
get runConfiguration() {
|
|
637
|
-
return CODEX_RUN_CONFIGURATION;
|
|
646
|
+
return this.nativeAccess?.runConfiguration ?? CODEX_RUN_CONFIGURATION;
|
|
638
647
|
}
|
|
639
648
|
async submitTurn(turn) {
|
|
640
649
|
if (this.#activeTurnId !== undefined) {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** No price catalog, currency conversion, token-to-money guess, or billing claim. */
|
|
2
|
+
export function projectTaskCosts(observations, inScope, workItem, missingEvidence = []) {
|
|
3
|
+
const main = observations.filter(o => o.fence.nativeSessionId !== undefined
|
|
4
|
+
&& o.fence.continuationId === undefined && o.fence.parentContinuationId === undefined
|
|
5
|
+
&& (o.authority === "provider-structured" || o.authority === "driver-inferred"));
|
|
6
|
+
const resource = (o) => JSON.stringify([
|
|
7
|
+
o.fence.driverId, o.fence.nativeSessionId
|
|
8
|
+
]);
|
|
9
|
+
const sessions = new Map();
|
|
10
|
+
for (const o of main)
|
|
11
|
+
sessions.set(resource(o), [...(sessions.get(resource(o)) ?? []), o]);
|
|
12
|
+
const project = (kind) => {
|
|
13
|
+
const evidence = [];
|
|
14
|
+
const reasons = [...missingEvidence];
|
|
15
|
+
if (observations.some(o => inScope(o) && o.payload.cost?.kind === kind
|
|
16
|
+
&& (o.fence.continuationId !== undefined || o.fence.parentContinuationId !== undefined))) {
|
|
17
|
+
reasons.push("child-usage-overlap-unproven");
|
|
18
|
+
}
|
|
19
|
+
for (const all of sessions.values()) {
|
|
20
|
+
if (!all.some(inScope))
|
|
21
|
+
continue;
|
|
22
|
+
const costs = all.filter(o => o.payload.cost?.kind === kind);
|
|
23
|
+
if (costs.length === 0) {
|
|
24
|
+
reasons.push("cost-unobserved");
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
// Different native Roles and sources cannot safely claim independent charges.
|
|
28
|
+
const overlap = new Set(all.map(o => JSON.stringify([o.fence.roleName, o.fence.agentId]))).size > 1;
|
|
29
|
+
const latest = costs.at(-1);
|
|
30
|
+
const cost = latest.payload.cost;
|
|
31
|
+
// Missing receipts can carry only a coverage boundary, with no cost.
|
|
32
|
+
const local = qualityReasons(all.filter(o => inScope(o)
|
|
33
|
+
&& (o.payload.cost === undefined || o.payload.cost.kind === kind)));
|
|
34
|
+
const semantics = new Set(costs.map(o => o.payload.cost.semantics));
|
|
35
|
+
if (overlap || semantics.size > 1) {
|
|
36
|
+
evidence.push(detail(latest, null, [...local,
|
|
37
|
+
overlap ? "native-session-role-overlap" : "cost-source-overlap"]));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (cost.semantics === "cumulative-session") {
|
|
41
|
+
let value = null;
|
|
42
|
+
if (new Set(costs.map(o => JSON.stringify([o.payload.cost.source, o.payload.cost.currency]))).size > 1) {
|
|
43
|
+
local.push("cost-source-overlap");
|
|
44
|
+
}
|
|
45
|
+
else if (workItem)
|
|
46
|
+
local.push("cumulative-not-allocatable-to-work-item");
|
|
47
|
+
else if (costs.some((o, i) => i > 0
|
|
48
|
+
&& o.payload.cost.amount < costs[i - 1].payload.cost.amount)) {
|
|
49
|
+
local.push("cost-counter-rollback");
|
|
50
|
+
}
|
|
51
|
+
else if (new Set(costs.map(o => JSON.stringify(o.payload.cost.basis))).size > 1) {
|
|
52
|
+
local.push("cost-basis-changed");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const baseline = costs[0].payload.cost.amount;
|
|
56
|
+
if (baseline !== 0)
|
|
57
|
+
local.push("nonzero-session-baseline");
|
|
58
|
+
if (baseline === 0 || costs.length > 1)
|
|
59
|
+
value = cost.amount - baseline;
|
|
60
|
+
}
|
|
61
|
+
evidence.push(detail(latest, value, [...local, ...qualityReasons(costs)]));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Currency and price basis can change in a request revision. Resolve
|
|
65
|
+
// identity and attribution over every version before monetary grouping.
|
|
66
|
+
const requests = new Map();
|
|
67
|
+
for (const o of costs) {
|
|
68
|
+
const id = o.payload.activityId;
|
|
69
|
+
requests.set(id, [...(requests.get(id) ?? []), o]);
|
|
70
|
+
}
|
|
71
|
+
const bases = new Map();
|
|
72
|
+
for (const request of requests.values()) {
|
|
73
|
+
if (!request.some(inScope))
|
|
74
|
+
continue;
|
|
75
|
+
const selected = request.at(-1);
|
|
76
|
+
const conflict = new Set(request.map(o => o.payload.cost.source)).size > 1
|
|
77
|
+
? "cost-source-overlap"
|
|
78
|
+
: workItem && (!request.every(inScope) || new Set(request.map(o => o.fence.runId)).size !== 1)
|
|
79
|
+
? "request-run-attribution-conflict" : undefined;
|
|
80
|
+
if (conflict !== undefined) {
|
|
81
|
+
evidence.push(detail(selected, null, [...local, conflict]));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const selectedCost = selected.payload.cost;
|
|
85
|
+
const key = JSON.stringify([selectedCost.currency, selectedCost.source, selectedCost.basis]);
|
|
86
|
+
bases.set(key, [...(bases.get(key) ?? []), selected]);
|
|
87
|
+
}
|
|
88
|
+
for (const group of bases.values()) {
|
|
89
|
+
const subtotal = group.reduce((sum, o) => sum + o.payload.cost.amount, 0);
|
|
90
|
+
evidence.push(detail(group.at(-1), safeAmount(subtotal), [...local, ...qualityReasons(group)]));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (evidence.length === 0)
|
|
94
|
+
reasons.push(kind === "estimated" ? "price-unavailable" : "cost-unobserved");
|
|
95
|
+
reasons.push(...evidence.flatMap(e => e.amount.reasons));
|
|
96
|
+
const amounts = [...new Set(evidence.map(e => e.currency))].sort().flatMap(currency => {
|
|
97
|
+
const known = evidence.filter(e => e.currency === currency && e.amount.value !== null);
|
|
98
|
+
return known.length === 0 ? [] : [{
|
|
99
|
+
...metric(safeAmount(known.reduce((sum, e) => sum + e.amount.value, 0)), reasons), currency
|
|
100
|
+
}];
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
status: amounts.length === 0 ? "unknown" : reasons.length ? "partial" : "known",
|
|
104
|
+
amounts, reasons: [...new Set(reasons)].sort(), evidence
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
return { scope: "observed-sources-only", actual: project("actual"), estimated: project("estimated") };
|
|
108
|
+
}
|
|
109
|
+
function qualityReasons(observations) {
|
|
110
|
+
return [
|
|
111
|
+
...(observations.some(o => o.payload.observationQuality === "partial"
|
|
112
|
+
|| o.payload.observationQuality === "unavailable") ? ["source-coverage-partial"] : []),
|
|
113
|
+
...(observations.some(o => (o.payload.cost?.basis?.excluded.length ?? 0) > 0)
|
|
114
|
+
? ["estimate-exclusions"] : [])
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
function detail(o, value, reasons) {
|
|
118
|
+
const cost = o.payload.cost;
|
|
119
|
+
return {
|
|
120
|
+
nativeSessionId: o.fence.nativeSessionId, roleName: o.fence.roleName,
|
|
121
|
+
agentId: o.fence.agentId, driverId: o.fence.driverId,
|
|
122
|
+
source: cost.source, currency: cost.currency, semantics: cost.semantics,
|
|
123
|
+
...(cost.basis === undefined ? {} : { basis: cost.basis }),
|
|
124
|
+
amount: metric(value, reasons)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function safeAmount(value) {
|
|
128
|
+
return Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER ? value : null;
|
|
129
|
+
}
|
|
130
|
+
function metric(value, reasons) {
|
|
131
|
+
return { value, status: value === null ? "unknown" : reasons.length ? "partial" : "known",
|
|
132
|
+
reasons: [...new Set(reasons)].sort() };
|
|
133
|
+
}
|