@hybridaione/hybridclaw 0.1.21 → 0.1.24
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/CHANGELOG.md +59 -0
- package/README.md +50 -8
- package/config.example.json +3 -0
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/browser-tools.ts +53 -3
- package/container/src/hybridai-client.ts +270 -8
- package/container/src/index.ts +66 -3
- package/container/src/token-usage.ts +89 -0
- package/container/src/tools.ts +9 -2
- package/container/src/types.ts +19 -0
- package/container/src/web-fetch.ts +98 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -2
- package/dist/agent.js.map +1 -1
- package/dist/chunk.d.ts +6 -0
- package/dist/chunk.d.ts.map +1 -0
- package/dist/chunk.js +129 -0
- package/dist/chunk.js.map +1 -0
- package/dist/container-runner.d.ts +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +25 -1
- package/dist/container-runner.js.map +1 -1
- package/dist/conversation.d.ts +4 -0
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +13 -3
- package/dist/conversation.js.map +1 -1
- package/dist/discord-stream.d.ts +32 -0
- package/dist/discord-stream.d.ts.map +1 -0
- package/dist/discord-stream.js +196 -0
- package/dist/discord-stream.js.map +1 -0
- package/dist/discord.d.ts +9 -2
- package/dist/discord.d.ts.map +1 -1
- package/dist/discord.js +452 -23
- package/dist/discord.js.map +1 -1
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +5 -0
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +1 -0
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +60 -2
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +7 -1
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +55 -4
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +7 -0
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +20 -0
- package/dist/heartbeat.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +26 -0
- package/dist/observability-ingest.js.map +1 -1
- package/dist/prompt-hooks.d.ts +2 -0
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +29 -0
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +3 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +17 -1
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +20 -0
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +1 -0
- package/dist/session-maintenance.js.map +1 -1
- package/dist/skills-guard.d.ts +36 -0
- package/dist/skills-guard.d.ts.map +1 -0
- package/dist/skills-guard.js +607 -0
- package/dist/skills-guard.js.map +1 -0
- package/dist/skills.d.ts +13 -2
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +494 -59
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts +41 -0
- package/dist/token-efficiency.d.ts.map +1 -0
- package/dist/token-efficiency.js +164 -0
- package/dist/token-efficiency.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +2 -1
- package/dist/workspace.js.map +1 -1
- package/docs/index.html +33 -7
- package/package.json +1 -1
- package/src/agent.ts +15 -1
- package/src/chunk.ts +153 -0
- package/src/container-runner.ts +24 -0
- package/src/conversation.ts +28 -4
- package/src/discord-stream.ts +240 -0
- package/src/discord.ts +517 -23
- package/src/gateway-client.ts +7 -0
- package/src/gateway-service.ts +72 -1
- package/src/gateway-types.ts +12 -1
- package/src/gateway.ts +65 -4
- package/src/health.ts +8 -0
- package/src/heartbeat.ts +20 -0
- package/src/observability-ingest.ts +24 -0
- package/src/prompt-hooks.ts +29 -0
- package/src/runtime-config.ts +18 -1
- package/src/scheduled-task-runner.ts +20 -0
- package/src/session-maintenance.ts +1 -0
- package/src/skills-guard.ts +736 -0
- package/src/skills.ts +570 -61
- package/src/token-efficiency.ts +228 -0
- package/src/types.ts +12 -0
- package/src/workspace.ts +2 -2
- package/.hybridclaw/container-image-state.json +0 -5
package/src/gateway-service.ts
CHANGED
|
@@ -50,16 +50,19 @@ import {
|
|
|
50
50
|
} from './gateway-types.js';
|
|
51
51
|
import type {
|
|
52
52
|
ArtifactMetadata,
|
|
53
|
+
ChatMessage,
|
|
53
54
|
DelegationSideEffect,
|
|
54
55
|
DelegationTaskSpec,
|
|
55
56
|
ScheduledTask,
|
|
56
57
|
StoredMessage,
|
|
58
|
+
TokenUsageStats,
|
|
57
59
|
ToolProgressEvent,
|
|
58
60
|
} from './types.js';
|
|
59
61
|
import { ensureBootstrapFiles } from './workspace.js';
|
|
60
62
|
import { buildConversationContext } from './conversation.js';
|
|
61
63
|
import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
|
|
62
64
|
import { delegationQueueStatus, enqueueDelegation } from './delegation-manager.js';
|
|
65
|
+
import { estimateTokenCountFromMessages, estimateTokenCountFromText } from './token-efficiency.js';
|
|
63
66
|
|
|
64
67
|
const BOT_CACHE_TTL = 300_000; // 5 minutes
|
|
65
68
|
const MAX_HISTORY_MESSAGES = 40;
|
|
@@ -166,6 +169,7 @@ export interface GatewayChatRequest {
|
|
|
166
169
|
chatbotId?: GatewayChatRequestBody['chatbotId'];
|
|
167
170
|
model?: GatewayChatRequestBody['model'];
|
|
168
171
|
enableRag?: GatewayChatRequestBody['enableRag'];
|
|
172
|
+
onTextDelta?: (delta: string) => void;
|
|
169
173
|
onToolProgress?: (event: ToolProgressEvent) => void;
|
|
170
174
|
onProactiveMessage?: (message: ProactiveMessagePayload) => void | Promise<void>;
|
|
171
175
|
abortSignal?: AbortSignal;
|
|
@@ -210,6 +214,49 @@ function plainCommand(text: string): GatewayCommandResult {
|
|
|
210
214
|
return { kind: 'plain', text };
|
|
211
215
|
}
|
|
212
216
|
|
|
217
|
+
function buildTokenUsageAuditPayload(
|
|
218
|
+
messages: ChatMessage[],
|
|
219
|
+
resultText: string | null | undefined,
|
|
220
|
+
tokenUsage?: TokenUsageStats,
|
|
221
|
+
): Record<string, number | boolean> {
|
|
222
|
+
const promptChars = messages.reduce((total, message) => {
|
|
223
|
+
const content = typeof message.content === 'string' ? message.content : '';
|
|
224
|
+
return total + content.length;
|
|
225
|
+
}, 0);
|
|
226
|
+
const completionChars = (resultText || '').length;
|
|
227
|
+
|
|
228
|
+
const fallbackEstimatedPromptTokens = estimateTokenCountFromMessages(messages);
|
|
229
|
+
const fallbackEstimatedCompletionTokens = estimateTokenCountFromText(resultText || '');
|
|
230
|
+
const estimatedPromptTokens = tokenUsage?.estimatedPromptTokens || fallbackEstimatedPromptTokens;
|
|
231
|
+
const estimatedCompletionTokens = tokenUsage?.estimatedCompletionTokens || fallbackEstimatedCompletionTokens;
|
|
232
|
+
const estimatedTotalTokens =
|
|
233
|
+
tokenUsage?.estimatedTotalTokens || (estimatedPromptTokens + estimatedCompletionTokens);
|
|
234
|
+
|
|
235
|
+
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
236
|
+
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
237
|
+
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
238
|
+
const apiTotalTokens = tokenUsage?.apiTotalTokens || (apiPromptTokens + apiCompletionTokens);
|
|
239
|
+
const promptTokens = apiUsageAvailable ? apiPromptTokens : estimatedPromptTokens;
|
|
240
|
+
const completionTokens = apiUsageAvailable ? apiCompletionTokens : estimatedCompletionTokens;
|
|
241
|
+
const totalTokens = apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens;
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
|
|
245
|
+
promptChars,
|
|
246
|
+
completionChars,
|
|
247
|
+
promptTokens,
|
|
248
|
+
completionTokens,
|
|
249
|
+
totalTokens,
|
|
250
|
+
estimatedPromptTokens,
|
|
251
|
+
estimatedCompletionTokens,
|
|
252
|
+
estimatedTotalTokens,
|
|
253
|
+
apiUsageAvailable,
|
|
254
|
+
apiPromptTokens,
|
|
255
|
+
apiCompletionTokens,
|
|
256
|
+
apiTotalTokens,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
213
260
|
export function getGatewayStatus(): GatewayStatus {
|
|
214
261
|
return {
|
|
215
262
|
status: 'ok',
|
|
@@ -811,11 +858,31 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
811
858
|
ensureBootstrapFiles(agentId);
|
|
812
859
|
|
|
813
860
|
const history = getConversationHistory(req.sessionId, MAX_HISTORY_MESSAGES);
|
|
814
|
-
const { messages, skills } = buildConversationContext({
|
|
861
|
+
const { messages, skills, historyStats } = buildConversationContext({
|
|
815
862
|
agentId,
|
|
816
863
|
sessionSummary: session.session_summary,
|
|
817
864
|
history,
|
|
818
865
|
});
|
|
866
|
+
const historyStart = messages.length > 0 && messages[0].role === 'system' ? 1 : 0;
|
|
867
|
+
recordAuditEvent({
|
|
868
|
+
sessionId: req.sessionId,
|
|
869
|
+
runId,
|
|
870
|
+
event: {
|
|
871
|
+
type: 'context.optimization',
|
|
872
|
+
historyMessagesOriginal: historyStats.originalCount,
|
|
873
|
+
historyMessagesIncluded: historyStats.includedCount,
|
|
874
|
+
historyMessagesDropped: historyStats.droppedCount,
|
|
875
|
+
historyCharsOriginal: historyStats.originalChars,
|
|
876
|
+
historyCharsPreBudget: historyStats.preBudgetChars,
|
|
877
|
+
historyCharsIncluded: historyStats.includedChars,
|
|
878
|
+
historyCharsDropped: historyStats.droppedChars,
|
|
879
|
+
historyMaxChars: historyStats.maxTotalChars,
|
|
880
|
+
historyMaxMessageChars: historyStats.maxMessageChars,
|
|
881
|
+
perMessageTruncatedCount: historyStats.perMessageTruncatedCount,
|
|
882
|
+
middleCompressionApplied: historyStats.middleCompressionApplied,
|
|
883
|
+
historyEstimatedTokens: estimateTokenCountFromMessages(messages.slice(historyStart)),
|
|
884
|
+
},
|
|
885
|
+
});
|
|
819
886
|
messages.push({
|
|
820
887
|
role: 'user',
|
|
821
888
|
content: expandSkillInvocation(req.content, skills),
|
|
@@ -833,6 +900,7 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
833
900
|
req.channelId,
|
|
834
901
|
scheduledTasks,
|
|
835
902
|
undefined,
|
|
903
|
+
req.onTextDelta,
|
|
836
904
|
req.onToolProgress,
|
|
837
905
|
req.abortSignal,
|
|
838
906
|
);
|
|
@@ -851,6 +919,7 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
851
919
|
model,
|
|
852
920
|
durationMs: Date.now() - startedAt,
|
|
853
921
|
toolCallCount: toolExecutions.length,
|
|
922
|
+
...buildTokenUsageAuditPayload(messages, output.result, output.tokenUsage),
|
|
854
923
|
},
|
|
855
924
|
});
|
|
856
925
|
|
|
@@ -944,6 +1013,7 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
944
1013
|
toolsUsed: output.toolsUsed || [],
|
|
945
1014
|
artifacts: output.artifacts,
|
|
946
1015
|
toolExecutions,
|
|
1016
|
+
tokenUsage: output.tokenUsage,
|
|
947
1017
|
error: errorMessage,
|
|
948
1018
|
};
|
|
949
1019
|
}
|
|
@@ -1009,6 +1079,7 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1009
1079
|
toolsUsed: output.toolsUsed || [],
|
|
1010
1080
|
artifacts: output.artifacts,
|
|
1011
1081
|
toolExecutions,
|
|
1082
|
+
tokenUsage: output.tokenUsage,
|
|
1012
1083
|
};
|
|
1013
1084
|
} catch (err) {
|
|
1014
1085
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
package/src/gateway-types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TokenUsageStats } from './types.js';
|
|
2
|
+
|
|
1
3
|
export interface GatewayCommandResult {
|
|
2
4
|
kind: 'plain' | 'info' | 'error';
|
|
3
5
|
title?: string;
|
|
@@ -19,6 +21,7 @@ export interface GatewayChatResult {
|
|
|
19
21
|
result: string;
|
|
20
22
|
durationMs: number;
|
|
21
23
|
}>;
|
|
24
|
+
tokenUsage?: TokenUsageStats;
|
|
22
25
|
error?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -30,12 +33,20 @@ export interface GatewayChatToolProgressEvent {
|
|
|
30
33
|
durationMs?: number;
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
export interface GatewayChatTextDeltaEvent {
|
|
37
|
+
type: 'text';
|
|
38
|
+
delta: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export interface GatewayChatStreamResultEvent {
|
|
34
42
|
type: 'result';
|
|
35
43
|
result: GatewayChatResult;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
export type GatewayChatStreamEvent =
|
|
46
|
+
export type GatewayChatStreamEvent =
|
|
47
|
+
| GatewayChatToolProgressEvent
|
|
48
|
+
| GatewayChatTextDeltaEvent
|
|
49
|
+
| GatewayChatStreamResultEvent;
|
|
39
50
|
|
|
40
51
|
export interface GatewayChatRequestBody {
|
|
41
52
|
sessionId: string;
|
package/src/gateway.ts
CHANGED
|
@@ -66,6 +66,53 @@ function buildArtifactAttachments(
|
|
|
66
66
|
return attachments;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function normalizePathForMatch(value: string): string {
|
|
70
|
+
return value.replace(/\\/g, '/').toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function simplifyImageAttachmentNarration(
|
|
74
|
+
text: string,
|
|
75
|
+
artifacts?: ArtifactMetadata[],
|
|
76
|
+
): string {
|
|
77
|
+
if (!text.trim() || !artifacts || artifacts.length === 0) return text;
|
|
78
|
+
|
|
79
|
+
const imageArtifacts = artifacts.filter((artifact) => artifact.mimeType.startsWith('image/'));
|
|
80
|
+
if (imageArtifacts.length === 0) return text;
|
|
81
|
+
|
|
82
|
+
const pathHints = new Set<string>();
|
|
83
|
+
for (const artifact of imageArtifacts) {
|
|
84
|
+
const normalizedPath = normalizePathForMatch(artifact.path);
|
|
85
|
+
const filename = normalizePathForMatch(artifact.filename);
|
|
86
|
+
if (normalizedPath) pathHints.add(normalizedPath);
|
|
87
|
+
if (filename) pathHints.add(filename);
|
|
88
|
+
if (filename) pathHints.add(`/workspace/.browser-artifacts/${filename}`);
|
|
89
|
+
if (filename) pathHints.add(`.browser-artifacts/${filename}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const pathishLine = /(^`?\s*(\.\/|\/|~\/|[a-zA-Z]:\\|\.browser-artifacts\/))|([\\/][^\\/\s]+\.[a-zA-Z0-9]{1,8})/;
|
|
93
|
+
const locationNarration = /(workspace|saved to|find it at|located at|liegt unter|pfad|path)/i;
|
|
94
|
+
|
|
95
|
+
let removedPathNarration = false;
|
|
96
|
+
const keptLines: string[] = [];
|
|
97
|
+
for (const line of text.split('\n')) {
|
|
98
|
+
const normalizedLine = normalizePathForMatch(line);
|
|
99
|
+
const mentionsArtifact = Array.from(pathHints).some((hint) => normalizedLine.includes(hint));
|
|
100
|
+
const isPathLine = pathishLine.test(line.trim());
|
|
101
|
+
const isLocationNarration = locationNarration.test(line);
|
|
102
|
+
if (mentionsArtifact && (isPathLine || isLocationNarration)) {
|
|
103
|
+
removedPathNarration = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
keptLines.push(line);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!removedPathNarration) return text;
|
|
110
|
+
|
|
111
|
+
const cleaned = keptLines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
112
|
+
if (cleaned) return cleaned;
|
|
113
|
+
return imageArtifacts.length === 1 ? 'Here it is.' : 'Here they are.';
|
|
114
|
+
}
|
|
115
|
+
|
|
69
116
|
async function deliverProactiveMessage(
|
|
70
117
|
channelId: string,
|
|
71
118
|
text: string,
|
|
@@ -151,7 +198,8 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
151
198
|
userId: string,
|
|
152
199
|
username: string,
|
|
153
200
|
content: string,
|
|
154
|
-
|
|
201
|
+
_reply: ReplyFn,
|
|
202
|
+
context,
|
|
155
203
|
) => {
|
|
156
204
|
try {
|
|
157
205
|
const result = await handleGatewayMessage({
|
|
@@ -161,20 +209,33 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
161
209
|
userId,
|
|
162
210
|
username,
|
|
163
211
|
content,
|
|
212
|
+
onTextDelta: (delta) => {
|
|
213
|
+
void context.stream.append(delta);
|
|
214
|
+
},
|
|
164
215
|
onProactiveMessage: async (message) => {
|
|
165
216
|
await deliverProactiveMessage(channelId, message.text, 'delegate', message.artifacts);
|
|
166
217
|
},
|
|
218
|
+
abortSignal: context.abortSignal,
|
|
167
219
|
});
|
|
168
220
|
if (result.status === 'error') {
|
|
169
|
-
|
|
221
|
+
const errorText = formatError('Agent Error', result.error || 'Unknown error');
|
|
222
|
+
await context.stream.fail(errorText);
|
|
170
223
|
return;
|
|
171
224
|
}
|
|
172
225
|
const attachments = buildArtifactAttachments(result.artifacts);
|
|
173
|
-
|
|
226
|
+
const userText = simplifyImageAttachmentNarration(
|
|
227
|
+
result.result || 'No response from agent.',
|
|
228
|
+
result.artifacts,
|
|
229
|
+
);
|
|
230
|
+
await context.stream.finalize(
|
|
231
|
+
buildResponseText(userText, result.toolsUsed),
|
|
232
|
+
attachments,
|
|
233
|
+
);
|
|
174
234
|
} catch (error) {
|
|
175
235
|
const text = error instanceof Error ? error.message : String(error);
|
|
176
236
|
logger.error({ error, sessionId, channelId }, 'Discord message handling failed');
|
|
177
|
-
|
|
237
|
+
const errorText = formatError('Gateway Error', text);
|
|
238
|
+
await context.stream.fail(errorText);
|
|
178
239
|
}
|
|
179
240
|
},
|
|
180
241
|
async (
|
package/src/health.ts
CHANGED
|
@@ -146,9 +146,17 @@ async function handleApiChatStream(
|
|
|
146
146
|
});
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
const onTextDelta = (delta: string): void => {
|
|
150
|
+
sendEvent({
|
|
151
|
+
type: 'text',
|
|
152
|
+
delta,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
149
156
|
try {
|
|
150
157
|
const result = await handleGatewayMessage({
|
|
151
158
|
...chatRequest,
|
|
159
|
+
onTextDelta,
|
|
152
160
|
onToolProgress,
|
|
153
161
|
});
|
|
154
162
|
sendEvent({ type: 'result', result });
|
package/src/heartbeat.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { appendSessionTranscript } from './session-transcripts.js';
|
|
|
12
12
|
import { buildConversationContext } from './conversation.js';
|
|
13
13
|
import { isWithinActiveHours, proactiveWindowLabel } from './proactive-policy.js';
|
|
14
14
|
import { emitToolExecutionAuditEvents, makeAuditRunId, recordAuditEvent } from './audit-events.js';
|
|
15
|
+
import { estimateTokenCountFromMessages, estimateTokenCountFromText } from './token-efficiency.js';
|
|
15
16
|
|
|
16
17
|
const HEARTBEAT_PROMPT =
|
|
17
18
|
'[Heartbeat poll] Check HEARTBEAT.md for periodic tasks. If nothing needs attention, reply HEARTBEAT_OK.';
|
|
@@ -136,6 +137,14 @@ export function startHeartbeat(
|
|
|
136
137
|
runId,
|
|
137
138
|
toolExecutions: output.toolExecutions || [],
|
|
138
139
|
});
|
|
140
|
+
const tokenUsage = output.tokenUsage;
|
|
141
|
+
const estimatedPromptTokens = tokenUsage?.estimatedPromptTokens || estimateTokenCountFromMessages(messages);
|
|
142
|
+
const estimatedCompletionTokens = tokenUsage?.estimatedCompletionTokens || estimateTokenCountFromText(output.result || '');
|
|
143
|
+
const estimatedTotalTokens = tokenUsage?.estimatedTotalTokens || (estimatedPromptTokens + estimatedCompletionTokens);
|
|
144
|
+
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
145
|
+
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
146
|
+
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
147
|
+
const apiTotalTokens = tokenUsage?.apiTotalTokens || (apiPromptTokens + apiCompletionTokens);
|
|
139
148
|
recordAuditEvent({
|
|
140
149
|
sessionId,
|
|
141
150
|
runId,
|
|
@@ -145,6 +154,17 @@ export function startHeartbeat(
|
|
|
145
154
|
model: HYBRIDAI_MODEL,
|
|
146
155
|
durationMs: Date.now() - startedAt,
|
|
147
156
|
toolCallCount: (output.toolExecutions || []).length,
|
|
157
|
+
modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
|
|
158
|
+
promptTokens: apiUsageAvailable ? apiPromptTokens : estimatedPromptTokens,
|
|
159
|
+
completionTokens: apiUsageAvailable ? apiCompletionTokens : estimatedCompletionTokens,
|
|
160
|
+
totalTokens: apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens,
|
|
161
|
+
estimatedPromptTokens,
|
|
162
|
+
estimatedCompletionTokens,
|
|
163
|
+
estimatedTotalTokens,
|
|
164
|
+
apiUsageAvailable,
|
|
165
|
+
apiPromptTokens,
|
|
166
|
+
apiCompletionTokens,
|
|
167
|
+
apiTotalTokens,
|
|
148
168
|
},
|
|
149
169
|
});
|
|
150
170
|
processSideEffects(output, sessionId, heartbeatChannelId);
|
|
@@ -222,6 +222,17 @@ function readNullableInteger(payload: Record<string, unknown>, key: string): num
|
|
|
222
222
|
return null;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
function readNullableBoolean(payload: Record<string, unknown>, key: string): boolean | null {
|
|
226
|
+
const value = payload[key];
|
|
227
|
+
if (typeof value === 'boolean') return value;
|
|
228
|
+
if (typeof value === 'string') {
|
|
229
|
+
const normalized = value.trim().toLowerCase();
|
|
230
|
+
if (normalized === 'true') return true;
|
|
231
|
+
if (normalized === 'false') return false;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
225
236
|
function inferDenied(payload: Record<string, unknown>): boolean {
|
|
226
237
|
if (typeof payload.denied === 'boolean') return payload.denied;
|
|
227
238
|
if (payload.approved === false) return true;
|
|
@@ -257,6 +268,19 @@ function mapAuditRowToEvent(config: ResolvedIngestConfig, row: StructuredAuditEn
|
|
|
257
268
|
denied: inferDenied(payload),
|
|
258
269
|
error_type: readNullableString(payload, 'errorType'),
|
|
259
270
|
duration_ms: readNullableInteger(payload, 'durationMs'),
|
|
271
|
+
model_calls: readNullableInteger(payload, 'modelCalls'),
|
|
272
|
+
prompt_chars: readNullableInteger(payload, 'promptChars'),
|
|
273
|
+
completion_chars: readNullableInteger(payload, 'completionChars'),
|
|
274
|
+
prompt_tokens: readNullableInteger(payload, 'promptTokens'),
|
|
275
|
+
completion_tokens: readNullableInteger(payload, 'completionTokens'),
|
|
276
|
+
total_tokens: readNullableInteger(payload, 'totalTokens'),
|
|
277
|
+
estimated_prompt_tokens: readNullableInteger(payload, 'estimatedPromptTokens'),
|
|
278
|
+
estimated_completion_tokens: readNullableInteger(payload, 'estimatedCompletionTokens'),
|
|
279
|
+
estimated_total_tokens: readNullableInteger(payload, 'estimatedTotalTokens'),
|
|
280
|
+
api_usage_available: readNullableBoolean(payload, 'apiUsageAvailable'),
|
|
281
|
+
api_prompt_tokens: readNullableInteger(payload, 'apiPromptTokens'),
|
|
282
|
+
api_completion_tokens: readNullableInteger(payload, 'apiCompletionTokens'),
|
|
283
|
+
api_total_tokens: readNullableInteger(payload, 'apiTotalTokens'),
|
|
260
284
|
event_uid: buildEventUid(config, row),
|
|
261
285
|
};
|
|
262
286
|
}
|
package/src/prompt-hooks.ts
CHANGED
|
@@ -6,12 +6,14 @@ import path from 'path';
|
|
|
6
6
|
|
|
7
7
|
export type PromptHookName = 'bootstrap' | 'memory' | 'safety';
|
|
8
8
|
export type ExtendedPromptHookName = PromptHookName | 'proactivity';
|
|
9
|
+
export type PromptMode = 'full' | 'minimal' | 'none';
|
|
9
10
|
|
|
10
11
|
export interface PromptHookContext {
|
|
11
12
|
agentId: string;
|
|
12
13
|
sessionSummary?: string | null;
|
|
13
14
|
skills: Skill[];
|
|
14
15
|
purpose?: 'conversation' | 'memory-flush';
|
|
16
|
+
promptMode?: PromptMode;
|
|
15
17
|
extraSafetyText?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -72,6 +74,17 @@ function buildSafetyHook(context: PromptHookContext): string {
|
|
|
72
74
|
'After file changes, run commands only when asked; otherwise explicitly offer to run them immediately.',
|
|
73
75
|
'Only skip file creation when the user explicitly asks for snippet-only or explanation-only output.',
|
|
74
76
|
'',
|
|
77
|
+
'## Web Retrieval Routing (web_fetch vs browser_*)',
|
|
78
|
+
'Decision rule: default to `web_fetch` for read-only content retrieval.',
|
|
79
|
+
'Use browser tools only when at least one of these is true: (1) known app-like/auth-gated URL, (2) interaction is required (click/type/login/scroll), (3) `web_fetch` returned escalation hints, (4) user explicitly requested browser use.',
|
|
80
|
+
'Prefer browser for: SPAs/client-rendered apps (React/Vue/Angular/Next client routes), dashboards/web apps, social feeds, login/OAuth/cookie-consent/CAPTCHA flows, or API-driven pages that populate after initial render.',
|
|
81
|
+
'Prefer web_fetch for: docs/wikis/READMEs/articles/reference pages, direct JSON/XML/text/CSV/PDF endpoints, and simple read-only extraction.',
|
|
82
|
+
'Escalation signals from web_fetch: `escalationHint` present, JavaScript-required pages, empty extraction, SPA shell-only pages, boilerplate-only extraction, or bot-blocked responses (403/429/challenge pages).',
|
|
83
|
+
'Cost note: browser calls are typically ~10-100x slower/more expensive than web_fetch.',
|
|
84
|
+
'Browser extraction flow (for read/summarize requests): after `browser_navigate`, call `browser_snapshot` with `mode="full"` before deciding content is unavailable.',
|
|
85
|
+
'If snapshot content is incomplete, run `browser_scroll` and then `browser_snapshot` again (repeat a few times for long/lazy-loaded pages).',
|
|
86
|
+
'Do not use `browser_pdf` as a text-reading step; it is an export artifact, not a text extraction tool.',
|
|
87
|
+
'',
|
|
75
88
|
'## Browser Auth Handling',
|
|
76
89
|
'When the user explicitly asks for login/auth-flow testing, browser tools may be used on the requested site, including filling credentials and submitting forms.',
|
|
77
90
|
'Do not invent blanket restrictions such as "browser tools are only for public/unauthenticated pages" unless an actual tool/policy error says so.',
|
|
@@ -206,11 +219,27 @@ const PROMPT_HOOKS: PromptHook[] = [
|
|
|
206
219
|
},
|
|
207
220
|
];
|
|
208
221
|
|
|
222
|
+
function resolvePromptMode(context: PromptHookContext): PromptMode {
|
|
223
|
+
if (context.promptMode === 'minimal' || context.promptMode === 'none') return context.promptMode;
|
|
224
|
+
return 'full';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isHookAllowedForMode(hookName: ExtendedPromptHookName, mode: PromptMode): boolean {
|
|
228
|
+
if (mode === 'none') return false;
|
|
229
|
+
if (mode === 'full') return true;
|
|
230
|
+
// Minimal mode keeps only safety + memory durability context.
|
|
231
|
+
return hookName === 'memory' || hookName === 'safety';
|
|
232
|
+
}
|
|
233
|
+
|
|
209
234
|
export function runPromptHooks(context: PromptHookContext): PromptHookOutput[] {
|
|
235
|
+
const mode = resolvePromptMode(context);
|
|
236
|
+
if (mode === 'none') return [];
|
|
237
|
+
|
|
210
238
|
const runtime = getRuntimeConfig();
|
|
211
239
|
const output: PromptHookOutput[] = [];
|
|
212
240
|
|
|
213
241
|
for (const hook of PROMPT_HOOKS) {
|
|
242
|
+
if (!isHookAllowedForMode(hook.name, mode)) continue;
|
|
214
243
|
if (!hook.isEnabled(runtime)) continue;
|
|
215
244
|
const content = hook.run(context).trim();
|
|
216
245
|
if (!content) continue;
|
package/src/runtime-config.ts
CHANGED
|
@@ -31,6 +31,9 @@ export interface RuntimeSecurityConfig {
|
|
|
31
31
|
export interface RuntimeConfig {
|
|
32
32
|
version: number;
|
|
33
33
|
security: RuntimeSecurityConfig;
|
|
34
|
+
skills: {
|
|
35
|
+
extraDirs: string[];
|
|
36
|
+
};
|
|
34
37
|
discord: {
|
|
35
38
|
prefix: string;
|
|
36
39
|
};
|
|
@@ -126,6 +129,9 @@ const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
|
|
|
126
129
|
trustModelVersion: '',
|
|
127
130
|
trustModelAcceptedBy: '',
|
|
128
131
|
},
|
|
132
|
+
skills: {
|
|
133
|
+
extraDirs: [],
|
|
134
|
+
},
|
|
129
135
|
discord: {
|
|
130
136
|
prefix: '!claw',
|
|
131
137
|
},
|
|
@@ -326,6 +332,7 @@ function readLegacyEnvPatch(): DeepPartial<RuntimeConfig> {
|
|
|
326
332
|
|
|
327
333
|
const patch: Record<string, unknown> = {
|
|
328
334
|
discord: {},
|
|
335
|
+
skills: {},
|
|
329
336
|
hybridai: {},
|
|
330
337
|
container: {},
|
|
331
338
|
heartbeat: {},
|
|
@@ -343,6 +350,7 @@ function readLegacyEnvPatch(): DeepPartial<RuntimeConfig> {
|
|
|
343
350
|
};
|
|
344
351
|
|
|
345
352
|
const discord = patch.discord as Record<string, unknown>;
|
|
353
|
+
const skills = patch.skills as Record<string, unknown>;
|
|
346
354
|
const hybridai = patch.hybridai as Record<string, unknown>;
|
|
347
355
|
const container = patch.container as Record<string, unknown>;
|
|
348
356
|
const heartbeat = patch.heartbeat as Record<string, unknown>;
|
|
@@ -356,6 +364,7 @@ function readLegacyEnvPatch(): DeepPartial<RuntimeConfig> {
|
|
|
356
364
|
const proactiveAutoRetry = proactive.autoRetry as Record<string, unknown>;
|
|
357
365
|
|
|
358
366
|
if (env.DISCORD_PREFIX != null) discord.prefix = env.DISCORD_PREFIX;
|
|
367
|
+
if (env.SKILLS_EXTRA_DIRS != null) skills.extraDirs = env.SKILLS_EXTRA_DIRS;
|
|
359
368
|
|
|
360
369
|
if (env.HYBRIDAI_BASE_URL != null) hybridai.baseUrl = env.HYBRIDAI_BASE_URL;
|
|
361
370
|
if (env.HYBRIDAI_MODEL != null) hybridai.defaultModel = env.HYBRIDAI_MODEL;
|
|
@@ -452,6 +461,7 @@ function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConf
|
|
|
452
461
|
const raw = patch ?? {};
|
|
453
462
|
|
|
454
463
|
const rawSecurity = isRecord(raw.security) ? raw.security : {};
|
|
464
|
+
const rawSkills = isRecord(raw.skills) ? raw.skills : {};
|
|
455
465
|
const rawDiscord = isRecord(raw.discord) ? raw.discord : {};
|
|
456
466
|
const rawHybridAi = isRecord(raw.hybridai) ? raw.hybridai : {};
|
|
457
467
|
const rawContainer = isRecord(raw.container) ? raw.container : {};
|
|
@@ -500,6 +510,9 @@ function normalizeRuntimeConfig(patch?: DeepPartial<RuntimeConfig>): RuntimeConf
|
|
|
500
510
|
trustModelVersion: normalizeString(rawSecurity.trustModelVersion, DEFAULT_RUNTIME_CONFIG.security.trustModelVersion, { allowEmpty: true }),
|
|
501
511
|
trustModelAcceptedBy: normalizeString(rawSecurity.trustModelAcceptedBy, DEFAULT_RUNTIME_CONFIG.security.trustModelAcceptedBy, { allowEmpty: true }),
|
|
502
512
|
},
|
|
513
|
+
skills: {
|
|
514
|
+
extraDirs: normalizeStringArray(rawSkills.extraDirs, DEFAULT_RUNTIME_CONFIG.skills.extraDirs),
|
|
515
|
+
},
|
|
503
516
|
discord: {
|
|
504
517
|
prefix: normalizeString(rawDiscord.prefix, DEFAULT_RUNTIME_CONFIG.discord.prefix, { allowEmpty: false }),
|
|
505
518
|
},
|
|
@@ -799,7 +812,11 @@ function migrateConfigSchemaOnStartup(): void {
|
|
|
799
812
|
try {
|
|
800
813
|
writeConfigFile(migrated);
|
|
801
814
|
const from = previousVersion == null ? 'unknown' : String(previousVersion);
|
|
802
|
-
|
|
815
|
+
if (previousVersion !== CONFIG_VERSION) {
|
|
816
|
+
console.info(`[runtime-config] migrated config schema from v${from} to v${CONFIG_VERSION}`);
|
|
817
|
+
} else {
|
|
818
|
+
console.info(`[runtime-config] normalized config schema v${CONFIG_VERSION} (filled defaults/canonicalized values)`);
|
|
819
|
+
}
|
|
803
820
|
} catch (err) {
|
|
804
821
|
console.warn(`[runtime-config] schema migration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
805
822
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runAgent } from './agent.js';
|
|
2
2
|
import { emitToolExecutionAuditEvents, makeAuditRunId, recordAuditEvent } from './audit-events.js';
|
|
3
3
|
import type { ChatMessage } from './types.js';
|
|
4
|
+
import { estimateTokenCountFromMessages, estimateTokenCountFromText } from './token-efficiency.js';
|
|
4
5
|
|
|
5
6
|
export async function runIsolatedScheduledTask(params: {
|
|
6
7
|
taskId: number;
|
|
@@ -53,6 +54,14 @@ export async function runIsolatedScheduledTask(params: {
|
|
|
53
54
|
runId,
|
|
54
55
|
toolExecutions: output.toolExecutions || [],
|
|
55
56
|
});
|
|
57
|
+
const tokenUsage = output.tokenUsage;
|
|
58
|
+
const estimatedPromptTokens = tokenUsage?.estimatedPromptTokens || estimateTokenCountFromMessages(messages);
|
|
59
|
+
const estimatedCompletionTokens = tokenUsage?.estimatedCompletionTokens || estimateTokenCountFromText(output.result || '');
|
|
60
|
+
const estimatedTotalTokens = tokenUsage?.estimatedTotalTokens || (estimatedPromptTokens + estimatedCompletionTokens);
|
|
61
|
+
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
62
|
+
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
63
|
+
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
64
|
+
const apiTotalTokens = tokenUsage?.apiTotalTokens || (apiPromptTokens + apiCompletionTokens);
|
|
56
65
|
recordAuditEvent({
|
|
57
66
|
sessionId: cronSessionId,
|
|
58
67
|
runId,
|
|
@@ -62,6 +71,17 @@ export async function runIsolatedScheduledTask(params: {
|
|
|
62
71
|
model,
|
|
63
72
|
durationMs: Date.now() - startedAt,
|
|
64
73
|
toolCallCount: (output.toolExecutions || []).length,
|
|
74
|
+
modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
|
|
75
|
+
promptTokens: apiUsageAvailable ? apiPromptTokens : estimatedPromptTokens,
|
|
76
|
+
completionTokens: apiUsageAvailable ? apiCompletionTokens : estimatedCompletionTokens,
|
|
77
|
+
totalTokens: apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens,
|
|
78
|
+
estimatedPromptTokens,
|
|
79
|
+
estimatedCompletionTokens,
|
|
80
|
+
estimatedTotalTokens,
|
|
81
|
+
apiUsageAvailable,
|
|
82
|
+
apiPromptTokens,
|
|
83
|
+
apiCompletionTokens,
|
|
84
|
+
apiTotalTokens,
|
|
65
85
|
},
|
|
66
86
|
});
|
|
67
87
|
|