@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +50 -8
  3. package/config.example.json +3 -0
  4. package/container/package-lock.json +2 -2
  5. package/container/package.json +1 -1
  6. package/container/src/browser-tools.ts +53 -3
  7. package/container/src/hybridai-client.ts +270 -8
  8. package/container/src/index.ts +66 -3
  9. package/container/src/token-usage.ts +89 -0
  10. package/container/src/tools.ts +9 -2
  11. package/container/src/types.ts +19 -0
  12. package/container/src/web-fetch.ts +98 -7
  13. package/dist/agent.d.ts +1 -1
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/agent.js +2 -2
  16. package/dist/agent.js.map +1 -1
  17. package/dist/chunk.d.ts +6 -0
  18. package/dist/chunk.d.ts.map +1 -0
  19. package/dist/chunk.js +129 -0
  20. package/dist/chunk.js.map +1 -0
  21. package/dist/container-runner.d.ts +1 -1
  22. package/dist/container-runner.d.ts.map +1 -1
  23. package/dist/container-runner.js +25 -1
  24. package/dist/container-runner.js.map +1 -1
  25. package/dist/conversation.d.ts +4 -0
  26. package/dist/conversation.d.ts.map +1 -1
  27. package/dist/conversation.js +13 -3
  28. package/dist/conversation.js.map +1 -1
  29. package/dist/discord-stream.d.ts +32 -0
  30. package/dist/discord-stream.d.ts.map +1 -0
  31. package/dist/discord-stream.js +196 -0
  32. package/dist/discord-stream.js.map +1 -0
  33. package/dist/discord.d.ts +9 -2
  34. package/dist/discord.d.ts.map +1 -1
  35. package/dist/discord.js +452 -23
  36. package/dist/discord.js.map +1 -1
  37. package/dist/gateway-client.d.ts.map +1 -1
  38. package/dist/gateway-client.js +5 -0
  39. package/dist/gateway-client.js.map +1 -1
  40. package/dist/gateway-service.d.ts +1 -0
  41. package/dist/gateway-service.d.ts.map +1 -1
  42. package/dist/gateway-service.js +60 -2
  43. package/dist/gateway-service.js.map +1 -1
  44. package/dist/gateway-types.d.ts +7 -1
  45. package/dist/gateway-types.d.ts.map +1 -1
  46. package/dist/gateway-types.js.map +1 -1
  47. package/dist/gateway.js +55 -4
  48. package/dist/gateway.js.map +1 -1
  49. package/dist/health.d.ts.map +1 -1
  50. package/dist/health.js +7 -0
  51. package/dist/health.js.map +1 -1
  52. package/dist/heartbeat.d.ts.map +1 -1
  53. package/dist/heartbeat.js +20 -0
  54. package/dist/heartbeat.js.map +1 -1
  55. package/dist/observability-ingest.d.ts.map +1 -1
  56. package/dist/observability-ingest.js +26 -0
  57. package/dist/observability-ingest.js.map +1 -1
  58. package/dist/prompt-hooks.d.ts +2 -0
  59. package/dist/prompt-hooks.d.ts.map +1 -1
  60. package/dist/prompt-hooks.js +29 -0
  61. package/dist/prompt-hooks.js.map +1 -1
  62. package/dist/runtime-config.d.ts +3 -0
  63. package/dist/runtime-config.d.ts.map +1 -1
  64. package/dist/runtime-config.js +17 -1
  65. package/dist/runtime-config.js.map +1 -1
  66. package/dist/scheduled-task-runner.d.ts.map +1 -1
  67. package/dist/scheduled-task-runner.js +20 -0
  68. package/dist/scheduled-task-runner.js.map +1 -1
  69. package/dist/session-maintenance.d.ts.map +1 -1
  70. package/dist/session-maintenance.js +1 -0
  71. package/dist/session-maintenance.js.map +1 -1
  72. package/dist/skills-guard.d.ts +36 -0
  73. package/dist/skills-guard.d.ts.map +1 -0
  74. package/dist/skills-guard.js +607 -0
  75. package/dist/skills-guard.js.map +1 -0
  76. package/dist/skills.d.ts +13 -2
  77. package/dist/skills.d.ts.map +1 -1
  78. package/dist/skills.js +494 -59
  79. package/dist/skills.js.map +1 -1
  80. package/dist/token-efficiency.d.ts +41 -0
  81. package/dist/token-efficiency.d.ts.map +1 -0
  82. package/dist/token-efficiency.js +164 -0
  83. package/dist/token-efficiency.js.map +1 -0
  84. package/dist/types.d.ts +11 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/workspace.d.ts.map +1 -1
  87. package/dist/workspace.js +2 -1
  88. package/dist/workspace.js.map +1 -1
  89. package/docs/index.html +33 -7
  90. package/package.json +1 -1
  91. package/src/agent.ts +15 -1
  92. package/src/chunk.ts +153 -0
  93. package/src/container-runner.ts +24 -0
  94. package/src/conversation.ts +28 -4
  95. package/src/discord-stream.ts +240 -0
  96. package/src/discord.ts +517 -23
  97. package/src/gateway-client.ts +7 -0
  98. package/src/gateway-service.ts +72 -1
  99. package/src/gateway-types.ts +12 -1
  100. package/src/gateway.ts +65 -4
  101. package/src/health.ts +8 -0
  102. package/src/heartbeat.ts +20 -0
  103. package/src/observability-ingest.ts +24 -0
  104. package/src/prompt-hooks.ts +29 -0
  105. package/src/runtime-config.ts +18 -1
  106. package/src/scheduled-task-runner.ts +20 -0
  107. package/src/session-maintenance.ts +1 -0
  108. package/src/skills-guard.ts +736 -0
  109. package/src/skills.ts +570 -61
  110. package/src/token-efficiency.ts +228 -0
  111. package/src/types.ts +12 -0
  112. package/src/workspace.ts +2 -2
  113. package/.hybridclaw/container-image-state.json +0 -5
@@ -0,0 +1,228 @@
1
+ import type { ChatMessage } from './types.js';
2
+
3
+ export const DEFAULT_CHARS_PER_TOKEN = 4;
4
+ export const DEFAULT_HISTORY_MAX_TOTAL_CHARS = 24_000;
5
+ export const DEFAULT_HISTORY_MAX_MESSAGE_CHARS = 1_200;
6
+ export const DEFAULT_HISTORY_PROTECT_HEAD_MESSAGES = 4;
7
+ export const DEFAULT_HISTORY_PROTECT_TAIL_MESSAGES = 8;
8
+ export const DEFAULT_BOOTSTRAP_HEAD_RATIO = 0.7;
9
+ export const DEFAULT_BOOTSTRAP_TAIL_RATIO = 0.2;
10
+
11
+ const MESSAGE_TRUNCATED_MARKER = '\n...[truncated]';
12
+ const HEAD_TAIL_TRUNCATED_MARKER = '\n\n...[truncated]...\n\n';
13
+
14
+ interface PromptHistoryMessage {
15
+ role: ChatMessage['role'];
16
+ content: string;
17
+ }
18
+
19
+ export interface HistoryOptimizationOptions {
20
+ maxTotalChars: number;
21
+ maxMessageChars: number;
22
+ protectHeadMessages: number;
23
+ protectTailMessages: number;
24
+ }
25
+
26
+ export interface HistoryOptimizationStats {
27
+ originalCount: number;
28
+ includedCount: number;
29
+ droppedCount: number;
30
+ originalChars: number;
31
+ preBudgetChars: number;
32
+ includedChars: number;
33
+ droppedChars: number;
34
+ maxTotalChars: number;
35
+ maxMessageChars: number;
36
+ perMessageTruncatedCount: number;
37
+ middleCompressionApplied: boolean;
38
+ }
39
+
40
+ function normalizePositiveInt(value: number, fallback: number): number {
41
+ if (!Number.isFinite(value) || value <= 0) return fallback;
42
+ return Math.floor(value);
43
+ }
44
+
45
+ function sumChars(messages: PromptHistoryMessage[]): number {
46
+ return messages.reduce((total, message) => total + message.content.length, 0);
47
+ }
48
+
49
+ function trimToRecentWithinBudget(
50
+ messages: PromptHistoryMessage[],
51
+ maxTotalChars: number,
52
+ ): PromptHistoryMessage[] {
53
+ if (messages.length === 0 || maxTotalChars <= 0) return [];
54
+
55
+ const kept: PromptHistoryMessage[] = [];
56
+ let usedChars = 0;
57
+
58
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
59
+ const message = messages[i];
60
+ const size = message.content.length;
61
+ if (size > maxTotalChars) continue;
62
+ if (usedChars + size > maxTotalChars) continue;
63
+ kept.push(message);
64
+ usedChars += size;
65
+ }
66
+
67
+ return kept.reverse();
68
+ }
69
+
70
+ export function estimateTokenCountFromText(text: string | null | undefined): number {
71
+ const normalized = typeof text === 'string' ? text : '';
72
+ if (!normalized) return 0;
73
+ return Math.max(1, Math.ceil(normalized.length / DEFAULT_CHARS_PER_TOKEN));
74
+ }
75
+
76
+ export function estimateTokenCountFromMessages(
77
+ messages: Array<Pick<ChatMessage, 'role' | 'content'>>,
78
+ ): number {
79
+ if (!Array.isArray(messages) || messages.length === 0) return 0;
80
+
81
+ let total = 2; // Approximate completion priming overhead.
82
+ for (const message of messages) {
83
+ total += 4; // Approximate per-message framing overhead.
84
+ total += estimateTokenCountFromText(message.role);
85
+ total += estimateTokenCountFromText(message.content);
86
+ }
87
+ return total;
88
+ }
89
+
90
+ export function truncateMessageContent(content: string, maxChars: number): string {
91
+ if (!Number.isFinite(maxChars) || maxChars <= 0) return '';
92
+ if (content.length <= maxChars) return content;
93
+
94
+ const bodyMax = Math.max(0, Math.floor(maxChars) - MESSAGE_TRUNCATED_MARKER.length);
95
+ if (bodyMax <= 0) {
96
+ return content.slice(0, Math.floor(maxChars));
97
+ }
98
+ return `${content.slice(0, bodyMax)}${MESSAGE_TRUNCATED_MARKER}`;
99
+ }
100
+
101
+ export function truncateHeadTailText(
102
+ content: string,
103
+ maxChars: number,
104
+ headRatio = DEFAULT_BOOTSTRAP_HEAD_RATIO,
105
+ tailRatio = DEFAULT_BOOTSTRAP_TAIL_RATIO,
106
+ ): string {
107
+ if (!Number.isFinite(maxChars) || maxChars <= 0) return '';
108
+ const budget = Math.floor(maxChars);
109
+ if (content.length <= budget) return content;
110
+
111
+ const marker = HEAD_TAIL_TRUNCATED_MARKER;
112
+ const available = budget - marker.length;
113
+ if (available <= 0) return content.slice(0, budget);
114
+
115
+ const clampedHeadRatio = Math.max(0, Math.min(1, headRatio));
116
+ const clampedTailRatio = Math.max(0, Math.min(1, tailRatio));
117
+
118
+ let headChars = Math.floor(available * clampedHeadRatio);
119
+ let tailChars = Math.floor(available * clampedTailRatio);
120
+ if (headChars + tailChars > available) {
121
+ const scale = available / (headChars + tailChars);
122
+ headChars = Math.floor(headChars * scale);
123
+ tailChars = Math.floor(tailChars * scale);
124
+ }
125
+
126
+ const remainder = available - (headChars + tailChars);
127
+ if (remainder > 0) {
128
+ headChars += remainder;
129
+ }
130
+
131
+ const safeHead = Math.max(0, Math.min(headChars, content.length));
132
+ const safeTail = Math.max(0, Math.min(tailChars, content.length - safeHead));
133
+ if (safeTail === 0) return `${content.slice(0, safeHead)}${marker}`;
134
+ return `${content.slice(0, safeHead)}${marker}${content.slice(content.length - safeTail)}`;
135
+ }
136
+
137
+ export function optimizeHistoryMessagesForPrompt(
138
+ messages: PromptHistoryMessage[],
139
+ options?: Partial<HistoryOptimizationOptions>,
140
+ ): { messages: PromptHistoryMessage[]; stats: HistoryOptimizationStats } {
141
+ const maxTotalChars = normalizePositiveInt(
142
+ options?.maxTotalChars ?? DEFAULT_HISTORY_MAX_TOTAL_CHARS,
143
+ DEFAULT_HISTORY_MAX_TOTAL_CHARS,
144
+ );
145
+ const maxMessageChars = normalizePositiveInt(
146
+ options?.maxMessageChars ?? DEFAULT_HISTORY_MAX_MESSAGE_CHARS,
147
+ DEFAULT_HISTORY_MAX_MESSAGE_CHARS,
148
+ );
149
+ const protectHeadMessages = Math.max(
150
+ 0,
151
+ Math.floor(options?.protectHeadMessages ?? DEFAULT_HISTORY_PROTECT_HEAD_MESSAGES),
152
+ );
153
+ const protectTailMessages = Math.max(
154
+ 0,
155
+ Math.floor(options?.protectTailMessages ?? DEFAULT_HISTORY_PROTECT_TAIL_MESSAGES),
156
+ );
157
+
158
+ const originalCount = messages.length;
159
+ const originalChars = messages.reduce((total, message) => total + message.content.length, 0);
160
+ let perMessageTruncatedCount = 0;
161
+
162
+ const normalized = messages.map((message) => {
163
+ const bounded = truncateMessageContent(message.content, maxMessageChars);
164
+ if (bounded !== message.content) perMessageTruncatedCount += 1;
165
+ return {
166
+ role: message.role,
167
+ content: bounded,
168
+ };
169
+ });
170
+
171
+ const preBudgetChars = sumChars(normalized);
172
+ let included = [...normalized];
173
+ let middleCompressionApplied = false;
174
+
175
+ if (preBudgetChars > maxTotalChars) {
176
+ middleCompressionApplied = true;
177
+ const headCount = Math.min(protectHeadMessages, normalized.length);
178
+ const tailCount = Math.min(protectTailMessages, Math.max(0, normalized.length - headCount));
179
+ const middleStart = headCount;
180
+ const middleEnd = normalized.length - tailCount;
181
+ const head = normalized.slice(0, headCount);
182
+ const middle = normalized.slice(middleStart, middleEnd);
183
+ const tail = normalized.slice(middleEnd);
184
+
185
+ const base = [...head, ...tail];
186
+ const baseChars = sumChars(base);
187
+
188
+ if (baseChars >= maxTotalChars) {
189
+ included = trimToRecentWithinBudget(base, maxTotalChars);
190
+ } else {
191
+ const selectedMiddleRev: PromptHistoryMessage[] = [];
192
+ let usedChars = baseChars;
193
+ for (let i = middle.length - 1; i >= 0; i -= 1) {
194
+ const candidate = middle[i];
195
+ const nextSize = candidate.content.length;
196
+ if (usedChars + nextSize > maxTotalChars) continue;
197
+ selectedMiddleRev.push(candidate);
198
+ usedChars += nextSize;
199
+ }
200
+ const selectedMiddle = selectedMiddleRev.reverse();
201
+ included = [...head, ...selectedMiddle, ...tail];
202
+ if (sumChars(included) > maxTotalChars) {
203
+ included = trimToRecentWithinBudget(included, maxTotalChars);
204
+ }
205
+ }
206
+ }
207
+
208
+ const includedChars = sumChars(included);
209
+ const droppedCount = Math.max(0, normalized.length - included.length);
210
+ const droppedChars = Math.max(0, preBudgetChars - includedChars);
211
+
212
+ return {
213
+ messages: included,
214
+ stats: {
215
+ originalCount,
216
+ includedCount: included.length,
217
+ droppedCount,
218
+ originalChars,
219
+ preBudgetChars,
220
+ includedChars,
221
+ droppedChars,
222
+ maxTotalChars,
223
+ maxMessageChars,
224
+ perMessageTruncatedCount,
225
+ middleCompressionApplied,
226
+ },
227
+ };
228
+ }
package/src/types.ts CHANGED
@@ -55,6 +55,17 @@ export interface ToolProgressEvent {
55
55
  durationMs?: number;
56
56
  }
57
57
 
58
+ export interface TokenUsageStats {
59
+ modelCalls: number;
60
+ apiUsageAvailable: boolean;
61
+ apiPromptTokens: number;
62
+ apiCompletionTokens: number;
63
+ apiTotalTokens: number;
64
+ estimatedPromptTokens: number;
65
+ estimatedCompletionTokens: number;
66
+ estimatedTotalTokens: number;
67
+ }
68
+
58
69
  export interface ArtifactMetadata {
59
70
  path: string;
60
71
  filename: string;
@@ -67,6 +78,7 @@ export interface ContainerOutput {
67
78
  toolsUsed: string[];
68
79
  artifacts?: ArtifactMetadata[];
69
80
  toolExecutions?: ToolExecution[];
81
+ tokenUsage?: TokenUsageStats;
70
82
  error?: string;
71
83
  sideEffects?: {
72
84
  schedules?: ScheduleSideEffect[];
package/src/workspace.ts CHANGED
@@ -8,6 +8,7 @@ import path from 'path';
8
8
 
9
9
  import { logger } from './logger.js';
10
10
  import { agentWorkspaceDir } from './ipc.js';
11
+ import { truncateHeadTailText } from './token-efficiency.js';
11
12
 
12
13
  const BOOTSTRAP_FILES = [
13
14
  'AGENTS.md',
@@ -65,7 +66,7 @@ export function loadBootstrapFiles(agentId: string): ContextFile[] {
65
66
  if (!content) continue;
66
67
 
67
68
  if (content.length > MAX_FILE_CHARS) {
68
- content = content.slice(0, MAX_FILE_CHARS) + '\n\n[truncated]';
69
+ content = truncateHeadTailText(content, MAX_FILE_CHARS);
69
70
  }
70
71
 
71
72
  files.push({ name: filename, content });
@@ -168,4 +169,3 @@ export function isBootstrapping(agentId: string): boolean {
168
169
 
169
170
  return true;
170
171
  }
171
-
@@ -1,5 +0,0 @@
1
- {
2
- "imageName": "hybridclaw-agent",
3
- "fingerprint": "7012ec282b43f0710cb509237a7560c5128d4c37055a935560431157e0142305",
4
- "recordedAt": "2026-03-02T23:05:53.092Z"
5
- }