fuli-context 0.9.0 → 0.11.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 +29 -58
- package/README.zh-CN.md +24 -54
- package/dist/web/assets/AboutPage-CKq4IRHd.js +1 -0
- package/dist/web/assets/AgentConversations-DfXiFvBN.js +2 -0
- package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
- package/dist/web/assets/AgentHand-BBM-sKh2.js +1 -0
- package/dist/web/assets/AgentName-C_6R80nP.js +1 -0
- package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
- package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +1 -0
- package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +3 -0
- package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
- package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
- package/dist/web/assets/KnowledgePage-kn8MqUCO.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +4 -0
- package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
- package/dist/web/assets/NotFoundPage-D-7jLz3o.js +1 -0
- package/dist/web/assets/OverviewPage-BHPc-XxY.js +1 -0
- package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
- package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +1 -0
- package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +12 -0
- package/dist/web/assets/ProjectAgentsPage-DNLNjYp-.css +1 -0
- package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +1 -0
- package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
- package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +1 -0
- package/dist/web/assets/ReviewPage-BZhvyDWc.js +1 -0
- package/dist/web/assets/RoundtablesPage-CVebTXQa.js +1 -0
- package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
- package/dist/web/assets/SettingsPage-1knvfOW6.js +1 -0
- package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
- package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +1 -0
- package/dist/web/assets/UiDialog-Da1P10f4.js +1 -0
- package/dist/web/assets/UiDisclosure-8wE8YuE4.js +1 -0
- package/dist/web/assets/UiSegmented-BKZXc8QF.js +1 -0
- package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
- package/dist/web/assets/UiSelect-D5Xj5Ec4.js +1 -0
- package/dist/web/assets/WritingTastePage-B8-etOBn.js +1 -0
- package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
- package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
- package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-BfxtL9EV.js} +1 -1
- package/dist/web/assets/{catalog-DN72452f.js → catalog-6SN9i3WR.js} +1 -1
- package/dist/web/assets/client-CUTkF3t4.js +1 -0
- package/dist/web/assets/{console-BtPqo79j.js → console-CG8tdtSs.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-CyZkLJg7.js} +1 -1
- package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-CCMR4gDS.js} +3 -3
- package/dist/web/assets/{identity-B3zEw-1u.js → identity-C1eN-FSB.js} +1 -1
- package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
- package/dist/web/assets/index-sLDKI6R-.js +5 -0
- package/dist/web/assets/{model-S7ehAjky.js → model-l-sAqtW_.js} +1 -1
- package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BD1Y6TDS.js} +1 -1
- package/dist/web/assets/shared-report-B46dxlCR.js +1 -0
- package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-DdxoKDEE.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-DlOiu8lP.js} +1 -1
- package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-DjqEU4W2.js} +1 -1
- package/dist/web/index.html +14 -16
- package/docs/agent-conversations-and-collaboration.md +9 -9
- package/docs/agent-roundtable.md +61 -0
- package/docs/employee-agents.md +2 -2
- package/docs/project-agent-memory.md +1 -1
- package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
- package/graph-provider/fuli_graph/conversation_context.py +40 -9
- package/graph-provider/fuli_graph/default_project_lead.py +88 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +7 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
- package/graph-provider/fuli_graph/runtime.py +3 -76
- package/graph-provider/fuli_graph/store.py +2 -0
- package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
- package/graph-provider/fuli_graph/store_project_agents.py +25 -124
- package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
- package/graph-provider/fuli_graph/system_agents.py +128 -0
- package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
- package/graph-provider/fuli_graph/task_context_routes.py +10 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -4
- package/skills/capturing-session-knowledge/SKILL.md +1 -1
- package/src/agent-roundtable/http.js +14 -0
- package/src/agent-roundtable/service.js +218 -0
- package/src/agent-roundtable/store.js +86 -0
- package/src/agent-roundtable/tools.js +61 -0
- package/src/agent-roundtable/wake.js +99 -0
- package/src/agent-tools/agent-conversation-definitions.js +3 -3
- package/src/agent-tools/graph-definitions.js +3 -2
- package/src/agent-tools/interface-catalog.js +0 -5
- package/src/agent-tools.js +2 -2
- package/src/agents/claude-code/lifecycle-hook.js +6 -19
- package/src/agents/claude-code/wake.js +37 -0
- package/src/agents/codex/lifecycle-hook.js +51 -27
- package/src/agents/codex/token-usage.js +235 -0
- package/src/agents/codex/wake.js +47 -0
- package/src/agents/cursor/lifecycle-hook.js +3 -9
- package/src/agents/identity-receipt.js +16 -6
- package/src/agents/session-log.js +34 -0
- package/src/agents/token-usage-resolver.js +147 -0
- package/src/agents/wake-registry.js +14 -0
- package/src/cli/command-registry.js +0 -2
- package/src/cli/setup-command.js +2 -5
- package/src/cli/update-command.js +13 -2
- package/src/cli.js +0 -5
- package/src/employees/management-policy.js +4 -2
- package/src/employees/manifest.js +3 -0
- package/src/employees/recruitment.js +3 -2
- package/src/graphiti/agent-conversations.js +11 -9
- package/src/graphiti/agent-knowledge-workflows.js +34 -7
- package/src/graphiti/federated-application.js +18 -6
- package/src/graphiti/project-agent-mapping.js +1 -1
- package/src/graphiti/project-agent-task-entry.js +19 -7
- package/src/graphiti/project-agent-workflows.js +6 -1
- package/src/graphiti/project-path-context.js +7 -2
- package/src/graphiti/provider-client.js +8 -0
- package/src/graphiti/repository-project-registration.js +17 -0
- package/src/graphiti/task-entry-preferences.js +5 -0
- package/src/http/api-router.js +5 -3
- package/src/http/error-mapping.js +0 -2
- package/src/mcp/instructions.js +5 -5
- package/src/mcp/session-id.js +1 -1
- package/src/mcp/tool-annotations.js +3 -2
- package/src/setup/agents.js +1 -41
- package/src/setup/graph-runtime.js +15 -2
- package/src/setup/session-skill.js +22 -2
- package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
- package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
- package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
- package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
- package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
- package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
- package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
- package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
- package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
- package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
- package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
- package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
- package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
- package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
- package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
- package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
- package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
- package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
- package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-BgQLIpxg.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-CQemHFxy.js +0 -12
- package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
- package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
- package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
- package/dist/web/assets/RoundtablesPage-ByVeZlgC.js +0 -2
- package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
- package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
- package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
- package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
- package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
- package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
- package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
- package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
- package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
- package/dist/web/assets/client-B2cRl-7c.js +0 -1
- package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
- package/dist/web/assets/index-CNp0tRNp.css +0 -1
- package/dist/web/assets/index-UygXwZDZ.js +0 -5
- package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
- package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
- package/docs/roundtable-beta-testing.md +0 -102
- package/docs/roundtable-beta-validation.md +0 -133
- package/docs/roundtable-research.md +0 -142
- package/src/agents/a2a/roundtable-participant.js +0 -374
- package/src/agents/claude-code/roundtable-participant.js +0 -24
- package/src/agents/codex/roundtable-config.js +0 -32
- package/src/agents/codex/roundtable-participant.js +0 -34
- package/src/agents/grok/roundtable-participant.js +0 -185
- package/src/agents/pi/roundtable-participant.js +0 -193
- package/src/agents/pi/roundtable-result-format.js +0 -183
- package/src/cli/roundtable-command.js +0 -79
- package/src/roundtables/application.js +0 -69
- package/src/roundtables/domain.js +0 -181
- package/src/roundtables/http-router.js +0 -56
- package/src/roundtables/mcp-server.js +0 -48
- package/src/roundtables/participant-registry.js +0 -12
- package/src/roundtables/process.js +0 -70
- package/src/roundtables/result-contract.js +0 -47
- package/src/roundtables/server.js +0 -52
- package/src/roundtables/service.js +0 -396
- package/src/roundtables/store.js +0 -59
- package/src/roundtables/tool-contract.js +0 -41
- package/src/roundtables/turn-prompt.js +0 -24
- package/src/roundtables/worker.js +0 -85
- /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
|
@@ -26,20 +26,9 @@ export async function codexStopLifecycleOutput(input, invoke) {
|
|
|
26
26
|
if (check?.decision !== 'block' || typeof check.reason !== 'string' || !check.reason.trim()) {
|
|
27
27
|
return {};
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
taskContextToken: check.task_context_token,
|
|
33
|
-
disposition: 'retain_nothing',
|
|
34
|
-
reason: 'Codex Stop hook fallback after one checkpoint continuation.',
|
|
35
|
-
workLog: { status: 'incomplete', summary: 'The host stopped before the employee submitted a work summary. Review the prior task before resuming.' },
|
|
36
|
-
sourceApplication: 'codex',
|
|
37
|
-
sourceSessionId: sessionId
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
return { decision: 'block', reason: boundedHookMessage(check.reason) };
|
|
29
|
+
// Checkpoints are advisory to the host. Never force another model turn or
|
|
30
|
+
// fabricate a work log merely because the user is ending the conversation.
|
|
31
|
+
return { systemMessage: boundedHookMessage(check.reason) };
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
export async function codexLifecycleOutput(event, input, invoke) {
|
|
@@ -73,6 +62,9 @@ export async function runCodexLifecycleHook(args, dependencies = {}) {
|
|
|
73
62
|
);
|
|
74
63
|
let app;
|
|
75
64
|
let leases;
|
|
65
|
+
let closing;
|
|
66
|
+
const cleanup = () => closing ??= Promise.allSettled([closeQuietly(leases), closeQuietly(app)]);
|
|
67
|
+
dependencies.onCleanup?.(cleanup);
|
|
76
68
|
try {
|
|
77
69
|
app = (dependencies.openApplication ?? openFederatedGraphApplication)({
|
|
78
70
|
runtimeConfigPath
|
|
@@ -94,32 +86,31 @@ export async function runCodexLifecycleHook(args, dependencies = {}) {
|
|
|
94
86
|
// Existing guards and all later failures remain fail-closed.
|
|
95
87
|
return { systemMessage: `Fuli: ${sync.reason}. Message submission is allowed; no saved Agent context was loaded or changed.` };
|
|
96
88
|
}
|
|
97
|
-
if (event === 'UserPromptSubmit' && sync.entryBlocked) return {
|
|
89
|
+
if (event === 'UserPromptSubmit' && sync.entryBlocked) return { systemMessage: boundedHookMessage(sync.reason) };
|
|
98
90
|
const entryGuard = sync.status === 'capture_disabled' ? null : guard;
|
|
99
91
|
if (event === 'UserPromptSubmit' && entryGuard?.value) await entryGuard.write({ ...entryGuard.value, phase: 'beginning' });
|
|
100
92
|
const result = await codexLifecycleOutput(event, input, async (name, parameters) => {
|
|
101
|
-
const context = await invokeTool(app, name, parameters);
|
|
93
|
+
const context = await invokeTool(app, name, parameters, { signal: dependencies.signal });
|
|
102
94
|
if (name === 'begin_task_context') await recordTranscriptTaskEntry(entryGuard, context);
|
|
103
95
|
return context;
|
|
104
96
|
});
|
|
105
97
|
if (event === 'UserPromptSubmit') sync = await claimTranscriptBoundary(app, input, 'codex', sync, {
|
|
106
98
|
normalize: normalizeCodexRecord, verify: verifyCodexTranscript
|
|
107
99
|
}, entryGuard);
|
|
108
|
-
if (event === 'UserPromptSubmit' && sync.entryBlocked) return {
|
|
100
|
+
if (event === 'UserPromptSubmit' && sync.entryBlocked) return { systemMessage: boundedHookMessage(sync.reason) };
|
|
109
101
|
return withTranscriptNotice(result, sync);
|
|
110
102
|
});
|
|
111
103
|
} catch (error) {
|
|
112
104
|
if (event !== 'UserPromptSubmit' || !app.getCapturePolicy?.().enabled || !input.transcript_path) throw error;
|
|
113
|
-
return {
|
|
105
|
+
return { systemMessage: error.code === 'EADDRINUSE'
|
|
114
106
|
? 'The session handoff lock is occupied. Retry after the current hook finishes; if it persists, start a new host session and resume the intended Agent.'
|
|
115
107
|
: 'Fuli transcript handoff could not be completed safely. Retry task entry; no previous Agent context was supplied.' };
|
|
116
108
|
}
|
|
117
|
-
});
|
|
109
|
+
}, { signal: dependencies.signal });
|
|
118
110
|
write(`${JSON.stringify(output)}\n`);
|
|
119
111
|
return output;
|
|
120
112
|
} finally {
|
|
121
|
-
await
|
|
122
|
-
await closeQuietly(app);
|
|
113
|
+
await cleanup();
|
|
123
114
|
}
|
|
124
115
|
}
|
|
125
116
|
|
|
@@ -142,13 +133,46 @@ async function readHookInput() {
|
|
|
142
133
|
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
143
134
|
}
|
|
144
135
|
|
|
145
|
-
async function
|
|
136
|
+
export async function runCodexLifecycleCommand(args, dependencies = {}) {
|
|
137
|
+
const configured = Number(args[args.indexOf('--timeout-ms') + 1]);
|
|
138
|
+
const timeoutMs = args.includes('--timeout-ms') && Number.isSafeInteger(configured) && configured > 0
|
|
139
|
+
? Math.min(configured, 5000) : 5000;
|
|
140
|
+
const controller = new AbortController();
|
|
141
|
+
let emitted = false;
|
|
142
|
+
const write = value => {
|
|
143
|
+
if (emitted) return;
|
|
144
|
+
emitted = true;
|
|
145
|
+
(dependencies.write ?? (value => process.stdout.write(value)))(value);
|
|
146
|
+
};
|
|
147
|
+
const unavailable = JSON.stringify({ systemMessage: 'Fuli context is unavailable. Continue with the user request; saved context and checkpoint were not verified.' }) + '\n';
|
|
148
|
+
let cleanup = async () => {};
|
|
149
|
+
let deadline;
|
|
150
|
+
const expired = new Promise(resolve => {
|
|
151
|
+
deadline = setTimeout(async () => {
|
|
152
|
+
controller.abort();
|
|
153
|
+
write(unavailable);
|
|
154
|
+
// A broken dependency must not hold the host open. Give opened leases
|
|
155
|
+
// and connections a bounded chance to close before the process exits.
|
|
156
|
+
let grace;
|
|
157
|
+
await Promise.race([cleanup(), new Promise(done => { grace = setTimeout(done, 250); })]);
|
|
158
|
+
clearTimeout(grace);
|
|
159
|
+
resolve({ timedOut: true });
|
|
160
|
+
}, timeoutMs);
|
|
161
|
+
});
|
|
146
162
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
const running = runCodexLifecycleHook(args, { ...dependencies, signal: controller.signal, write,
|
|
164
|
+
onCleanup: close => { cleanup = close; } }).then(() => ({ timedOut: false }), () => {
|
|
165
|
+
// Never print prompts, transcripts, paths or credentials.
|
|
166
|
+
write(unavailable);
|
|
167
|
+
return { timedOut: false };
|
|
168
|
+
});
|
|
169
|
+
return await Promise.race([running, expired]);
|
|
170
|
+
} finally {
|
|
171
|
+
clearTimeout(deadline);
|
|
151
172
|
}
|
|
152
173
|
}
|
|
153
174
|
|
|
154
|
-
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
175
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
176
|
+
const result = await runCodexLifecycleCommand(process.argv.slice(2));
|
|
177
|
+
if (result.timedOut) process.exit(0);
|
|
178
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, opendir, open } from 'node:fs/promises';
|
|
3
|
+
import { basename, extname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const MAX_HEADER_BYTES = 64 * 1024;
|
|
6
|
+
const MAX_USAGE_TAIL_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
const MAX_CANDIDATE_FILES = 128;
|
|
8
|
+
const MAX_DIRECTORY_ENTRIES = 2048;
|
|
9
|
+
const MAX_DIRECTORY_DEPTH = 8;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Codex's native JSONL session adapter. It returns only the latest cumulative
|
|
13
|
+
* token_count snapshot and never returns transcript records or text.
|
|
14
|
+
*/
|
|
15
|
+
export function createCodexTokenUsageResolver({
|
|
16
|
+
sessionsDirectory = undefined,
|
|
17
|
+
archivedSessionsDirectory = undefined,
|
|
18
|
+
codexSessionsDirectory = undefined,
|
|
19
|
+
codexArchivedSessionsDirectory = undefined,
|
|
20
|
+
codexHomeDirectory = undefined,
|
|
21
|
+
homeDirectory = undefined
|
|
22
|
+
} = {}) {
|
|
23
|
+
const explicitSessionsDirectory = sessionsDirectory ?? codexSessionsDirectory;
|
|
24
|
+
const explicitArchivedSessionsDirectory =
|
|
25
|
+
archivedSessionsDirectory ?? codexArchivedSessionsDirectory;
|
|
26
|
+
const home = typeof homeDirectory === 'string' && homeDirectory
|
|
27
|
+
? homeDirectory : defaultHomeDirectory();
|
|
28
|
+
const codexHome = typeof codexHomeDirectory === 'string' && codexHomeDirectory
|
|
29
|
+
? codexHomeDirectory
|
|
30
|
+
: homeDirectory === undefined ? (defaultCodexHomeDirectory() ?? (home ? join(home, '.codex') : null))
|
|
31
|
+
: (home ? join(home, '.codex') : null);
|
|
32
|
+
const useDefaultRoots = explicitSessionsDirectory === undefined &&
|
|
33
|
+
explicitArchivedSessionsDirectory === undefined;
|
|
34
|
+
const roots = [
|
|
35
|
+
explicitSessionsDirectory ?? (useDefaultRoots && codexHome ? join(codexHome, 'sessions') : null),
|
|
36
|
+
explicitArchivedSessionsDirectory ??
|
|
37
|
+
(useDefaultRoots && codexHome ? join(codexHome, 'archived_sessions') : null)
|
|
38
|
+
]
|
|
39
|
+
.filter((root, index, values) => typeof root === 'string' && root && values.indexOf(root) === index);
|
|
40
|
+
return {
|
|
41
|
+
resolve: (request) => resolveCodexTokenUsage(request, { roots })
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function resolveCodexTokenUsage(request = {}, { roots } = {}) {
|
|
46
|
+
const sessionId = safeSessionId(request.sessionId ?? request.session_id);
|
|
47
|
+
if (!sessionId) return null;
|
|
48
|
+
const configuredRoots = Array.isArray(roots) ? roots : roots ? [roots] : null;
|
|
49
|
+
const directories = (configuredRoots ?? [
|
|
50
|
+
defaultCodexSessionsDirectory(),
|
|
51
|
+
defaultCodexArchivedSessionsDirectory()
|
|
52
|
+
]).filter((root, index, values) => typeof root === 'string' && root && values.indexOf(root) === index);
|
|
53
|
+
|
|
54
|
+
let inspectedEntries = 0;
|
|
55
|
+
let candidates = 0;
|
|
56
|
+
for (const root of directories) {
|
|
57
|
+
const files = await matchingSessionFiles(root, sessionId, {
|
|
58
|
+
onEntry: () => { inspectedEntries += 1; },
|
|
59
|
+
get entriesInspected() { return inspectedEntries; },
|
|
60
|
+
get candidatesFound() { return candidates; },
|
|
61
|
+
addCandidate: () => { candidates += 1; }
|
|
62
|
+
});
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const usage = await readSessionUsage(file, sessionId);
|
|
65
|
+
if (usage) return usage;
|
|
66
|
+
}
|
|
67
|
+
if (inspectedEntries >= MAX_DIRECTORY_ENTRIES || candidates >= MAX_CANDIDATE_FILES) break;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function matchingSessionFiles(root, sessionId, counters, depth = 0) {
|
|
73
|
+
if (depth > MAX_DIRECTORY_DEPTH || counters.entriesInspected >= MAX_DIRECTORY_ENTRIES ||
|
|
74
|
+
counters.candidatesFound >= MAX_CANDIDATE_FILES) return [];
|
|
75
|
+
let directory;
|
|
76
|
+
try {
|
|
77
|
+
const rootStat = await lstat(root);
|
|
78
|
+
if (!rootStat.isDirectory()) return [];
|
|
79
|
+
directory = await opendir(root);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const files = [];
|
|
85
|
+
try {
|
|
86
|
+
for await (const entry of directory) {
|
|
87
|
+
counters.onEntry();
|
|
88
|
+
if (counters.entriesInspected > MAX_DIRECTORY_ENTRIES ||
|
|
89
|
+
counters.candidatesFound >= MAX_CANDIDATE_FILES) break;
|
|
90
|
+
const path = join(root, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
files.push(...await matchingSessionFiles(path, sessionId, counters, depth + 1));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!entry.isFile() || extname(entry.name) !== '.jsonl' ||
|
|
96
|
+
!basename(entry.name).includes(sessionId)) continue;
|
|
97
|
+
counters.addCandidate();
|
|
98
|
+
files.push(path);
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// A session directory can rotate while the host is running. An incomplete
|
|
102
|
+
// scan is an unavailable snapshot, never a reason to guess another file.
|
|
103
|
+
} finally {
|
|
104
|
+
await directory.close().catch(() => {});
|
|
105
|
+
}
|
|
106
|
+
return files;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readSessionUsage(filePath, sessionId) {
|
|
110
|
+
let file;
|
|
111
|
+
try {
|
|
112
|
+
file = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
113
|
+
const stat = await file.stat();
|
|
114
|
+
if (!stat.isFile() || stat.size <= 0) return null;
|
|
115
|
+
|
|
116
|
+
const header = await readBytes(file, Math.min(MAX_HEADER_BYTES, stat.size), 0);
|
|
117
|
+
const firstLineEnd = header.indexOf(10);
|
|
118
|
+
if (firstLineEnd < 0 || !isSessionHeader(header.subarray(0, firstLineEnd), sessionId)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const start = Math.max(0, stat.size - MAX_USAGE_TAIL_BYTES);
|
|
123
|
+
const tail = await readBytes(file, stat.size - start, start);
|
|
124
|
+
let firstLine = 0;
|
|
125
|
+
if (start > 0) {
|
|
126
|
+
const previous = await readBytes(file, 1, start - 1);
|
|
127
|
+
if (previous[0] !== 10) {
|
|
128
|
+
firstLine = tail.indexOf(10) + 1;
|
|
129
|
+
if (firstLine <= 0) return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return latestTokenUsage(tail.subarray(firstLine));
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
} finally {
|
|
136
|
+
await file?.close().catch(() => {});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function readBytes(file, length, position) {
|
|
141
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
142
|
+
let offset = 0;
|
|
143
|
+
while (offset < length) {
|
|
144
|
+
const result = await file.read(buffer, offset, length - offset, position + offset);
|
|
145
|
+
if (!result.bytesRead) break;
|
|
146
|
+
offset += result.bytesRead;
|
|
147
|
+
}
|
|
148
|
+
return offset === length ? buffer : buffer.subarray(0, offset);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isSessionHeader(bytes, sessionId) {
|
|
152
|
+
try {
|
|
153
|
+
const record = JSON.parse(bytes.toString('utf8'));
|
|
154
|
+
return record?.type === 'session_meta' && record.payload?.id === sessionId;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function latestTokenUsage(bytes) {
|
|
161
|
+
let latest = null;
|
|
162
|
+
const text = bytes.toString('utf8');
|
|
163
|
+
let offset = 0;
|
|
164
|
+
while (offset < text.length) {
|
|
165
|
+
const end = text.indexOf('\n', offset);
|
|
166
|
+
const line = text.slice(offset, end < 0 ? text.length : end).trim();
|
|
167
|
+
if (line.includes('"token_count"') && line.includes('"total_token_usage"')) {
|
|
168
|
+
try {
|
|
169
|
+
const record = JSON.parse(line);
|
|
170
|
+
const total = record?.payload?.info?.total_token_usage;
|
|
171
|
+
if (record?.type === 'event_msg' && record.payload?.type === 'token_count') {
|
|
172
|
+
const totalTokens = nonNegativeInteger(total?.total_tokens);
|
|
173
|
+
if (totalTokens !== null) {
|
|
174
|
+
latest = {
|
|
175
|
+
source: 'host',
|
|
176
|
+
totalTokens,
|
|
177
|
+
inputTokens: optionalNonNegativeInteger(total?.input_tokens),
|
|
178
|
+
outputTokens: optionalNonNegativeInteger(total?.output_tokens),
|
|
179
|
+
cachedInputTokens: optionalNonNegativeInteger(total?.cached_input_tokens),
|
|
180
|
+
cacheWriteInputTokens: optionalNonNegativeInteger(total?.cache_write_input_tokens),
|
|
181
|
+
reasoningOutputTokens: optionalNonNegativeInteger(total?.reasoning_output_tokens)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// Ignore malformed native records and continue to later snapshots.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (end < 0) break;
|
|
190
|
+
offset = end + 1;
|
|
191
|
+
}
|
|
192
|
+
return latest;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function optionalNonNegativeInteger(value) {
|
|
196
|
+
return value === null || value === undefined ? null : nonNegativeInteger(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function nonNegativeInteger(value) {
|
|
200
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function safeSessionId(value) {
|
|
204
|
+
if (typeof value !== 'string') return null;
|
|
205
|
+
const normalized = value.trim();
|
|
206
|
+
if (!normalized || normalized.length > 256 || normalized.includes('..') ||
|
|
207
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) return null;
|
|
208
|
+
return normalized;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function defaultCodexSessionsDirectory() {
|
|
212
|
+
const codexHome = defaultCodexRootDirectory();
|
|
213
|
+
return codexHome ? join(codexHome, 'sessions') : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function defaultCodexArchivedSessionsDirectory() {
|
|
217
|
+
const codexHome = defaultCodexRootDirectory();
|
|
218
|
+
return codexHome ? join(codexHome, 'archived_sessions') : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function defaultCodexRootDirectory() {
|
|
222
|
+
const configured = defaultCodexHomeDirectory();
|
|
223
|
+
if (configured) return configured;
|
|
224
|
+
const home = defaultHomeDirectory();
|
|
225
|
+
return home ? join(home, '.codex') : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function defaultCodexHomeDirectory() {
|
|
229
|
+
const value = process.env.CODEX_HOME;
|
|
230
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function defaultHomeDirectory() {
|
|
234
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? null;
|
|
235
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { findFile, firstField, listDirectory } from '../session-log.js';
|
|
6
|
+
|
|
7
|
+
// Wakes Codex headlessly with a read-only sandbox. Resuming records the exchange in
|
|
8
|
+
// that conversation; Codex refuses while the conversation is open in the app.
|
|
9
|
+
export const codexWake = Object.freeze({
|
|
10
|
+
id: 'codex',
|
|
11
|
+
|
|
12
|
+
resolveCommand({ env, which, platform, fileExists = existsSync }) {
|
|
13
|
+
if (env.FULI_CODEX_BIN) return env.FULI_CODEX_BIN;
|
|
14
|
+
const found = which('codex', platform);
|
|
15
|
+
if (found) return found;
|
|
16
|
+
if (platform === 'win32') return newestBundledCodex(join(env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), 'OpenAI', 'Codex', 'bin'));
|
|
17
|
+
const bundled = platform === 'darwin' ? '/Applications/Codex.app/Contents/Resources/codex' : null;
|
|
18
|
+
return bundled && fileExists(bundled) ? bundled : null;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
args({ sessionId, cwd }) {
|
|
22
|
+
return sessionId
|
|
23
|
+
? ['exec', 'resume', '--json', '--skip-git-repo-check', '-c', 'sandbox_mode="read-only"', sessionId, '-']
|
|
24
|
+
: ['exec', '--json', '--skip-git-repo-check', '--sandbox', 'read-only', '-C', cwd, '-'];
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// A session resumes only from the directory it started in.
|
|
28
|
+
sessionDirectory(sessionId, { home, env }) {
|
|
29
|
+
const file = findFile(join(env.CODEX_HOME || join(home, '.codex'), 'sessions'), `${sessionId}.jsonl`, 4);
|
|
30
|
+
return file ? firstField(file, (line) => line.type === 'session_meta' ? line.payload?.cwd : null) : null;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// `codex exec --json` streams items; the last agent message before turn.completed is the answer.
|
|
34
|
+
initialState: () => ({ body: '', completed: false, failed: false }),
|
|
35
|
+
readEvent(event, state) {
|
|
36
|
+
if (event.type === 'item.completed' && event.item?.type === 'agent_message') state.body = event.item.text ?? '';
|
|
37
|
+
if (event.type === 'turn.completed') state.completed = true;
|
|
38
|
+
if (event.type === 'turn.failed' || event.type === 'error') state.failed = true;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// The Codex app keeps each CLI version in its own folder; the newest is current.
|
|
43
|
+
function newestBundledCodex(root) {
|
|
44
|
+
const candidates = [join(root, 'codex.exe'), ...listDirectory(root).map((entry) => join(root, entry, 'codex.exe'))]
|
|
45
|
+
.filter((path) => existsSync(path));
|
|
46
|
+
return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0] ?? null;
|
|
47
|
+
}
|
|
@@ -6,7 +6,6 @@ import { callAgentTool } from '../../agent-tools.js';
|
|
|
6
6
|
import { createRuntimeLeaseClient } from '../../adaptive-runtime/lease-client.js';
|
|
7
7
|
import { openFederatedGraphApplication } from '../../graphiti/federated-application.js';
|
|
8
8
|
import { resolveGraphRuntimeOptions } from '../../graphiti/runtime-config.js';
|
|
9
|
-
import { boundedHookMessage } from '../lifecycle-message.js';
|
|
10
9
|
|
|
11
10
|
const EVENTS = new Set(['sessionStart', 'beforeSubmitPrompt', 'stop']);
|
|
12
11
|
|
|
@@ -18,14 +17,9 @@ export async function cursorLifecycleOutput(event, input, invoke) {
|
|
|
18
17
|
}
|
|
19
18
|
const source = { sourceApplication: 'cursor', sourceSessionId: sessionId };
|
|
20
19
|
if (event === 'stop') {
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const notice = '\nIf this mode is read-only or checkpoint writes are denied, state that the checkpoint was not saved and do not retry. This is the only checkpoint continuation.';
|
|
25
|
-
const reason = boundedHookMessage(check.reason, { maxBytes: 8_000 - Buffer.byteLength(notice, 'utf8') });
|
|
26
|
-
return check.status === 'checkpoint_required' && reason.trim()
|
|
27
|
-
? { followup_message: reason + notice }
|
|
28
|
-
: {};
|
|
20
|
+
// Cursor has no advisory Stop output. A followup_message would start
|
|
21
|
+
// another model turn, so pending checkpoints remain available through MCP.
|
|
22
|
+
return {};
|
|
29
23
|
}
|
|
30
24
|
const roots = input.workspace_roots;
|
|
31
25
|
if (!Array.isArray(roots) || roots.length !== 1 || typeof roots[0] !== 'string') {
|
|
@@ -12,20 +12,30 @@ export function agentContinuation(projectId, agentId, conversationId = null) {
|
|
|
12
12
|
guidance: 'Use a connected client and the same authorized project/Agent. Obtain a fresh task context; never reuse the previous client’s token. This is a FULI prompt, not a native client @ picker registration.' };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
sourceApplication = null, workStatus = null, conversationId = null, persistence = null }) {
|
|
17
|
-
const owner = agentId ? { agent_id: agentId, name,
|
|
18
|
-
profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
|
|
15
|
+
function agentLink(application, agentId, name) {
|
|
19
16
|
const label = String(name?.trim() || agentId).replace(/[\r\n\t]+/g, ' ')
|
|
20
17
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
21
18
|
.replace(/[\\`*_{}\[\]()!|]/g, '\\$&');
|
|
19
|
+
return `[${label}](<${agentProfileUrl(application, agentId).replace(/[<>\s]/g, encodeURIComponent)}>)`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The reply's attribution line: who led the task and who actually helped.
|
|
23
|
+
export function taskAgentReceipt(application, { projectId, agentId, name = null,
|
|
24
|
+
sourceApplication = null, workStatus = null, conversationId = null, persistence = null,
|
|
25
|
+
collaborators = [] }) {
|
|
26
|
+
const owner = agentId ? { agent_id: agentId, name,
|
|
27
|
+
profile_url: agentProfileUrl(application, agentId), source_application: sourceApplication } : null;
|
|
28
|
+
const helpers = owner ? collaborators.filter(item => item.agentId !== agentId) : [];
|
|
22
29
|
const markdown = owner
|
|
23
|
-
? `FULI Agent
|
|
30
|
+
? `FULI Agent:负责人 ${agentLink(application, agentId, name)}`
|
|
31
|
+
+ (helpers.length ? ` · 协作 ${helpers.map(item => agentLink(application, item.agentId, item.name)).join('、')}` : '')
|
|
24
32
|
: 'FULI Agent:未选定';
|
|
25
33
|
return { required: true, markdown, owner,
|
|
34
|
+
collaborators: helpers.map(item => ({ agent_id: item.agentId, name: item.name,
|
|
35
|
+
profile_url: agentProfileUrl(application, item.agentId) })),
|
|
26
36
|
personal_project_id: projectId, work_status: workStatus, persistence,
|
|
27
37
|
continuation: owner ? agentContinuation(projectId, agentId, conversationId) : null,
|
|
28
|
-
guidance: 'MUST
|
|
38
|
+
guidance: 'MUST end every final user-visible reply with agent_receipt.markdown unchanged, exactly once, including greetings, status and errors, even when capture is disabled or nothing is retained. The receipt returned by checkpoint_task_knowledge supersedes the entry receipt because it names actual collaborators. Never invent an owner or a collaborator.' };
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export function employeeCollaborationReceipt(application, context, { tool, permission, sourceApplication }) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Read-only helpers for locating a client's JSONL session log on disk.
|
|
5
|
+
|
|
6
|
+
export function listDirectory(directory) {
|
|
7
|
+
try { return readdirSync(directory); } catch { return []; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Newest-first search for a file ending in `suffix`, at most `depth` levels down.
|
|
11
|
+
export function findFile(directory, suffix, depth) {
|
|
12
|
+
for (const entry of listDirectory(directory).sort().reverse()) {
|
|
13
|
+
const path = join(directory, entry);
|
|
14
|
+
if (entry.endsWith(suffix)) return path;
|
|
15
|
+
if (depth > 0 && !entry.includes('.')) {
|
|
16
|
+
const found = findFile(path, suffix, depth - 1);
|
|
17
|
+
if (found) return found;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The first non-empty string `pick` returns for a JSON line near the start of the log.
|
|
24
|
+
export function firstField(file, pick) {
|
|
25
|
+
const handle = openSync(file, 'r');
|
|
26
|
+
try {
|
|
27
|
+
const buffer = Buffer.alloc(256 * 1024);
|
|
28
|
+
const length = readSync(handle, buffer, 0, buffer.length, 0);
|
|
29
|
+
for (const line of buffer.subarray(0, length).toString('utf8').split('\n')) {
|
|
30
|
+
try { const value = pick(JSON.parse(line)); if (typeof value === 'string' && value) return value; } catch { /* partial line */ }
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
} finally { closeSync(handle); }
|
|
34
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createCodexTokenUsageResolver } from './codex/token-usage.js';
|
|
2
|
+
|
|
3
|
+
const TOKEN_USAGE_SOURCES = new Set(['executor', 'host', 'dingdong']);
|
|
4
|
+
const DEFAULT_RESOLVER = createAgentTokenUsageResolver();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve one exact worker/session identity through the runtime adapter port.
|
|
8
|
+
*
|
|
9
|
+
* A worker runtime is authoritative when present. Its session is never
|
|
10
|
+
* replaced with the reporting host session. A reporting host session is used
|
|
11
|
+
* only when no worker runtime was supplied, which covers direct host activity.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveAgentSession(input = {}) {
|
|
14
|
+
const workerRuntime = input.workerRuntime ?? input.worker_runtime;
|
|
15
|
+
if (workerRuntime !== undefined && workerRuntime !== null) {
|
|
16
|
+
if (!workerRuntime || typeof workerRuntime !== 'object' || Array.isArray(workerRuntime)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const application = workerRuntime.application;
|
|
20
|
+
const sessionId = safeSessionId(workerRuntime.sessionId ?? workerRuntime.session_id);
|
|
21
|
+
if (!validApplication(application) || !sessionId) return null;
|
|
22
|
+
return {
|
|
23
|
+
application,
|
|
24
|
+
sessionId,
|
|
25
|
+
sourceApplication: input.sourceApplication ?? input.source_application ?? null,
|
|
26
|
+
sourceSessionId: input.sourceSessionId ?? input.source_session_id ?? null,
|
|
27
|
+
scope: 'worker'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (hasWorkerMetadata(input)) return null;
|
|
32
|
+
const application = input.sourceApplication ?? input.source_application;
|
|
33
|
+
const sessionId = safeSessionId(input.sourceSessionId ?? input.source_session_id);
|
|
34
|
+
if (!validApplication(application) || !sessionId) return null;
|
|
35
|
+
return {
|
|
36
|
+
application,
|
|
37
|
+
sessionId,
|
|
38
|
+
sourceApplication: application,
|
|
39
|
+
sourceSessionId: sessionId,
|
|
40
|
+
scope: 'host'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Shared adapter port for exact session token usage.
|
|
46
|
+
*
|
|
47
|
+
* Adapters receive only the selected session identity and return a cumulative
|
|
48
|
+
* snapshot or null. They must not return transcript content.
|
|
49
|
+
*/
|
|
50
|
+
export function createAgentTokenUsageResolver({ adapters } = {}) {
|
|
51
|
+
const registry = adapters instanceof Map
|
|
52
|
+
? adapters
|
|
53
|
+
: new Map(Object.entries(adapters ?? {
|
|
54
|
+
codex: createCodexTokenUsageResolver()
|
|
55
|
+
}));
|
|
56
|
+
return {
|
|
57
|
+
async resolve(input = {}) {
|
|
58
|
+
const session = resolveAgentSession(input);
|
|
59
|
+
if (!session) return null;
|
|
60
|
+
const adapter = registry.get(session.application);
|
|
61
|
+
const resolve = typeof adapter === 'function' ? adapter : adapter?.resolve;
|
|
62
|
+
if (typeof resolve !== 'function') return null;
|
|
63
|
+
try {
|
|
64
|
+
return normalizeTokenUsage(await resolve.call(adapter, session));
|
|
65
|
+
} catch {
|
|
66
|
+
// Usage collection is best-effort. It must not prevent the host from
|
|
67
|
+
// recording otherwise valid worker activity.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function enrichProjectAgentTaskActivity(input = {}, resolver = DEFAULT_RESOLVER) {
|
|
75
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
|
|
76
|
+
const actorKind = input.actorKind ?? input.actor_kind;
|
|
77
|
+
if (actorKind === 'human') return input;
|
|
78
|
+
// Null is an explicit report (including an explicit zero inside the object),
|
|
79
|
+
// so only an omitted value is eligible for automatic collection.
|
|
80
|
+
const explicitTokenUsage = (Object.hasOwn(input, 'tokenUsage') && input.tokenUsage !== undefined) ||
|
|
81
|
+
(Object.hasOwn(input, 'token_usage') && input.token_usage !== undefined);
|
|
82
|
+
if (explicitTokenUsage) return input;
|
|
83
|
+
|
|
84
|
+
const resolve = typeof resolver === 'function' ? resolver : resolver?.resolve;
|
|
85
|
+
if (typeof resolve !== 'function') return input;
|
|
86
|
+
try {
|
|
87
|
+
const tokenUsage = normalizeTokenUsage(await resolve.call(resolver, input));
|
|
88
|
+
return tokenUsage ? { ...input, tokenUsage } : input;
|
|
89
|
+
} catch {
|
|
90
|
+
return input;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function resolveProjectAgentTaskActivityTokenUsage(input, resolver = DEFAULT_RESOLVER) {
|
|
95
|
+
const enriched = await enrichProjectAgentTaskActivity(input, resolver);
|
|
96
|
+
if (!enriched || typeof enriched !== 'object') return null;
|
|
97
|
+
return enriched.tokenUsage !== undefined ? enriched.tokenUsage : enriched.token_usage ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeTokenUsage(value) {
|
|
101
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
102
|
+
const source = value.source;
|
|
103
|
+
const totalTokens = nonNegativeInteger(value.totalTokens ?? value.total_tokens);
|
|
104
|
+
if (!TOKEN_USAGE_SOURCES.has(source) || totalTokens === null) return null;
|
|
105
|
+
return {
|
|
106
|
+
source,
|
|
107
|
+
totalTokens,
|
|
108
|
+
inputTokens: optionalNonNegativeInteger(value.inputTokens ?? value.input_tokens),
|
|
109
|
+
outputTokens: optionalNonNegativeInteger(value.outputTokens ?? value.output_tokens),
|
|
110
|
+
cachedInputTokens: optionalNonNegativeInteger(value.cachedInputTokens ?? value.cached_input_tokens),
|
|
111
|
+
cacheWriteInputTokens: optionalNonNegativeInteger(
|
|
112
|
+
value.cacheWriteInputTokens ?? value.cache_write_input_tokens
|
|
113
|
+
),
|
|
114
|
+
reasoningOutputTokens: optionalNonNegativeInteger(
|
|
115
|
+
value.reasoningOutputTokens ?? value.reasoning_output_tokens
|
|
116
|
+
)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function optionalNonNegativeInteger(value) {
|
|
121
|
+
return value === null || value === undefined ? null : nonNegativeInteger(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function nonNegativeInteger(value) {
|
|
125
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validApplication(value) {
|
|
129
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 64;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasWorkerMetadata(input) {
|
|
133
|
+
return [
|
|
134
|
+
input.workerId ?? input.worker_id,
|
|
135
|
+
input.workerLabel ?? input.worker_label,
|
|
136
|
+
input.workerStatus ?? input.worker_status,
|
|
137
|
+
input.workerOccupationEmoji ?? input.worker_occupation_emoji
|
|
138
|
+
].some((value) => value !== undefined && value !== null && value !== '');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function safeSessionId(value) {
|
|
142
|
+
if (typeof value !== 'string') return null;
|
|
143
|
+
const normalized = value.trim();
|
|
144
|
+
if (!normalized || normalized.length > 256 || normalized.includes('..') ||
|
|
145
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) return null;
|
|
146
|
+
return normalized;
|
|
147
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { claudeCodeWake } from './claude-code/wake.js';
|
|
2
|
+
import { codexWake } from './codex/wake.js';
|
|
3
|
+
|
|
4
|
+
// Clients that can be woken headlessly to answer another Agent, in preference order.
|
|
5
|
+
export const WAKE_ADAPTERS = Object.freeze({
|
|
6
|
+
[claudeCodeWake.id]: claudeCodeWake,
|
|
7
|
+
[codexWake.id]: codexWake,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const WAKE_CLIENTS = Object.freeze(Object.keys(WAKE_ADAPTERS));
|
|
11
|
+
|
|
12
|
+
export function wakeAdapter(client) {
|
|
13
|
+
return Object.hasOwn(WAKE_ADAPTERS, client) ? WAKE_ADAPTERS[client] : null;
|
|
14
|
+
}
|