@poncho-ai/harness 0.28.0 → 0.28.1
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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +11 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +152 -23
- package/package.json +2 -2
- package/src/harness.ts +18 -3
- package/src/search-tools.ts +98 -12
- package/src/skill-tools.ts +9 -7
- package/src/state.ts +52 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.28.
|
|
2
|
+
> @poncho-ai/harness@0.28.1 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
12
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m288.74 KB[39m
|
|
12
|
+
[32mESM[39m ⚡️ Build success in 135ms
|
|
13
13
|
[34mDTS[39m Build start
|
|
14
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m29.
|
|
14
|
+
[32mDTS[39m ⚡️ Build success in 6964ms
|
|
15
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m29.62 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.28.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`4d50ad9`](https://github.com/cesr/poncho-ai/commit/4d50ad970886c9d3635ec36a407514c91ce6a71a) Thanks [@cesr](https://github.com/cesr)! - Improve callback-run reliability and streaming across subagent workflows, including safer concurrent approval handling and parent callback retriggers.
|
|
8
|
+
|
|
9
|
+
Add context window/token reporting through run completion events, improve cron/web UI rendering and approval streaming behavior, and harden built-in web search retry/throttle behavior.
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`4d50ad9`](https://github.com/cesr/poncho-ai/commit/4d50ad970886c9d3635ec36a407514c91ce6a71a)]:
|
|
12
|
+
- @poncho-ai/sdk@1.6.1
|
|
13
|
+
|
|
3
14
|
## 0.28.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -188,6 +188,12 @@ interface ConversationStore {
|
|
|
188
188
|
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
189
189
|
delete(conversationId: string): Promise<boolean>;
|
|
190
190
|
appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Atomically clear `runningCallbackSince` without clobbering other fields.
|
|
193
|
+
* Returns the conversation as it exists after the clear (with current
|
|
194
|
+
* `pendingSubagentResults`).
|
|
195
|
+
*/
|
|
196
|
+
clearCallbackLock(conversationId: string): Promise<Conversation | undefined>;
|
|
191
197
|
}
|
|
192
198
|
type StateProviderName = "local" | "memory" | "redis" | "upstash" | "dynamodb";
|
|
193
199
|
interface StateConfig {
|
|
@@ -221,6 +227,7 @@ declare class InMemoryConversationStore implements ConversationStore {
|
|
|
221
227
|
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
222
228
|
delete(conversationId: string): Promise<boolean>;
|
|
223
229
|
appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
|
|
230
|
+
clearCallbackLock(conversationId: string): Promise<Conversation | undefined>;
|
|
224
231
|
}
|
|
225
232
|
type ConversationSummary = {
|
|
226
233
|
conversationId: string;
|
package/dist/index.js
CHANGED
|
@@ -4211,15 +4211,16 @@ var loadRunnableScriptFunction = async (scriptPath) => {
|
|
|
4211
4211
|
};
|
|
4212
4212
|
var loadScriptModule = async (scriptPath) => {
|
|
4213
4213
|
const extension = extname(scriptPath).toLowerCase();
|
|
4214
|
+
const cacheBust = `?t=${Date.now()}`;
|
|
4214
4215
|
if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
|
|
4215
|
-
const jiti = createJiti2(import.meta.url, { interopDefault: true });
|
|
4216
|
-
return await jiti.import(scriptPath);
|
|
4216
|
+
const jiti = createJiti2(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
4217
|
+
return await jiti.import(scriptPath + cacheBust);
|
|
4217
4218
|
}
|
|
4218
4219
|
try {
|
|
4219
|
-
return await import(pathToFileURL(scriptPath).href);
|
|
4220
|
+
return await import(pathToFileURL(scriptPath).href + cacheBust);
|
|
4220
4221
|
} catch {
|
|
4221
|
-
const jiti = createJiti2(import.meta.url, { interopDefault: true });
|
|
4222
|
-
return await jiti.import(scriptPath);
|
|
4222
|
+
const jiti = createJiti2(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
4223
|
+
return await jiti.import(scriptPath + cacheBust);
|
|
4223
4224
|
}
|
|
4224
4225
|
};
|
|
4225
4226
|
var extractRunnableFunction = (value) => {
|
|
@@ -4263,21 +4264,93 @@ import { load as cheerioLoad } from "cheerio";
|
|
|
4263
4264
|
import { defineTool as defineTool5 } from "@poncho-ai/sdk";
|
|
4264
4265
|
var SEARCH_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
4265
4266
|
var FETCH_TIMEOUT_MS = 15e3;
|
|
4267
|
+
var SEARCH_MAX_RETRIES = 4;
|
|
4268
|
+
var SEARCH_INITIAL_DELAY_MS = 2e3;
|
|
4269
|
+
var SEARCH_MIN_INTERVAL_MS = 4e3;
|
|
4270
|
+
var SEARCH_FALLBACK_COOLDOWN_MS = 12e3;
|
|
4271
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4272
|
+
var searchQueue = Promise.resolve();
|
|
4273
|
+
var nextSearchAllowedAt = 0;
|
|
4274
|
+
function parseRetryAfterMs(retryAfterHeader) {
|
|
4275
|
+
if (!retryAfterHeader) return SEARCH_FALLBACK_COOLDOWN_MS;
|
|
4276
|
+
const asSeconds = Number(retryAfterHeader);
|
|
4277
|
+
if (Number.isFinite(asSeconds) && asSeconds >= 0) {
|
|
4278
|
+
return Math.max(Math.floor(asSeconds * 1e3), SEARCH_MIN_INTERVAL_MS);
|
|
4279
|
+
}
|
|
4280
|
+
const asDate = new Date(retryAfterHeader).getTime();
|
|
4281
|
+
if (Number.isFinite(asDate)) {
|
|
4282
|
+
return Math.max(asDate - Date.now(), SEARCH_MIN_INTERVAL_MS);
|
|
4283
|
+
}
|
|
4284
|
+
return SEARCH_FALLBACK_COOLDOWN_MS;
|
|
4285
|
+
}
|
|
4286
|
+
function applyRateLimitCooldown(retryAfterHeader) {
|
|
4287
|
+
const cooldownMs = parseRetryAfterMs(retryAfterHeader);
|
|
4288
|
+
nextSearchAllowedAt = Math.max(nextSearchAllowedAt, Date.now() + cooldownMs);
|
|
4289
|
+
}
|
|
4290
|
+
async function runWithSearchThrottle(fn) {
|
|
4291
|
+
const previous = searchQueue;
|
|
4292
|
+
let release;
|
|
4293
|
+
searchQueue = new Promise((resolve12) => {
|
|
4294
|
+
release = resolve12;
|
|
4295
|
+
});
|
|
4296
|
+
await previous.catch(() => {
|
|
4297
|
+
});
|
|
4298
|
+
try {
|
|
4299
|
+
const waitMs = nextSearchAllowedAt - Date.now();
|
|
4300
|
+
if (waitMs > 0) {
|
|
4301
|
+
await sleep(waitMs);
|
|
4302
|
+
}
|
|
4303
|
+
const result = await fn();
|
|
4304
|
+
nextSearchAllowedAt = Math.max(nextSearchAllowedAt, Date.now() + SEARCH_MIN_INTERVAL_MS);
|
|
4305
|
+
return result;
|
|
4306
|
+
} finally {
|
|
4307
|
+
release?.();
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
function isRetryableStatus(status) {
|
|
4311
|
+
return status === 429 || status === 503 || status >= 500;
|
|
4312
|
+
}
|
|
4266
4313
|
async function braveSearch(query, maxResults) {
|
|
4267
4314
|
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4315
|
+
let lastError;
|
|
4316
|
+
for (let attempt = 0; attempt < SEARCH_MAX_RETRIES; attempt++) {
|
|
4317
|
+
if (attempt > 0) {
|
|
4318
|
+
const delay = SEARCH_INITIAL_DELAY_MS * 2 ** (attempt - 1) + Math.floor(Math.random() * 500);
|
|
4319
|
+
await sleep(delay);
|
|
4320
|
+
}
|
|
4321
|
+
try {
|
|
4322
|
+
const html = await runWithSearchThrottle(async () => {
|
|
4323
|
+
const res = await fetch(url, {
|
|
4324
|
+
headers: {
|
|
4325
|
+
"User-Agent": SEARCH_UA,
|
|
4326
|
+
Accept: "text/html,application/xhtml+xml",
|
|
4327
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
4328
|
+
},
|
|
4329
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
4330
|
+
});
|
|
4331
|
+
if (!res.ok) {
|
|
4332
|
+
if (res.status === 429) {
|
|
4333
|
+
applyRateLimitCooldown(res.headers.get("retry-after"));
|
|
4334
|
+
}
|
|
4335
|
+
const error = new Error(`Search request failed (${res.status} ${res.statusText})`);
|
|
4336
|
+
if (isRetryableStatus(res.status)) {
|
|
4337
|
+
throw error;
|
|
4338
|
+
}
|
|
4339
|
+
error.retryable = false;
|
|
4340
|
+
throw error;
|
|
4341
|
+
}
|
|
4342
|
+
return await res.text();
|
|
4343
|
+
});
|
|
4344
|
+
return parseBraveResults(html, maxResults);
|
|
4345
|
+
} catch (err) {
|
|
4346
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
4347
|
+
if (lastError.retryable === false) {
|
|
4348
|
+
throw lastError;
|
|
4349
|
+
}
|
|
4350
|
+
if (attempt < SEARCH_MAX_RETRIES - 1) continue;
|
|
4351
|
+
}
|
|
4278
4352
|
}
|
|
4279
|
-
|
|
4280
|
-
return parseBraveResults(html, maxResults);
|
|
4353
|
+
throw lastError ?? new Error("Search failed after retries");
|
|
4281
4354
|
}
|
|
4282
4355
|
function parseBraveResults(html, max) {
|
|
4283
4356
|
const $ = cheerioLoad(html);
|
|
@@ -5878,6 +5951,8 @@ ${this.skillFingerprint}`;
|
|
|
5878
5951
|
let totalOutputTokens = 0;
|
|
5879
5952
|
let totalCachedTokens = 0;
|
|
5880
5953
|
let transientStepRetryCount = 0;
|
|
5954
|
+
let latestContextTokens = 0;
|
|
5955
|
+
let toolOutputEstimateSinceModel = 0;
|
|
5881
5956
|
let cachedCoreMessages = [];
|
|
5882
5957
|
let convertedUpTo = 0;
|
|
5883
5958
|
for (let step = 1; step <= maxSteps; step += 1) {
|
|
@@ -5907,7 +5982,9 @@ ${this.skillFingerprint}`;
|
|
|
5907
5982
|
duration: now() - start,
|
|
5908
5983
|
continuation: true,
|
|
5909
5984
|
continuationMessages: [...messages],
|
|
5910
|
-
maxSteps
|
|
5985
|
+
maxSteps,
|
|
5986
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
5987
|
+
contextWindow
|
|
5911
5988
|
};
|
|
5912
5989
|
yield pushEvent({ type: "run:completed", runId, result: result2 });
|
|
5913
5990
|
return;
|
|
@@ -6147,10 +6224,13 @@ ${textContent}` };
|
|
|
6147
6224
|
if (emittedMessages.length > 0 && emittedMessages[emittedMessages.length - 1].role === "user") {
|
|
6148
6225
|
emittedMessages.pop();
|
|
6149
6226
|
}
|
|
6227
|
+
const tokensAfterCompaction = estimateTotalTokens(integrityPrompt, messages, toolDefsJson);
|
|
6228
|
+
latestContextTokens = tokensAfterCompaction;
|
|
6229
|
+
toolOutputEstimateSinceModel = 0;
|
|
6150
6230
|
yield pushEvent({
|
|
6151
6231
|
type: "compaction:completed",
|
|
6152
6232
|
tokensBefore: effectiveTokens,
|
|
6153
|
-
tokensAfter:
|
|
6233
|
+
tokensAfter: tokensAfterCompaction,
|
|
6154
6234
|
messagesBefore: compactResult.messagesBefore,
|
|
6155
6235
|
compactedMessages: emittedMessages,
|
|
6156
6236
|
messagesAfter: compactResult.messagesAfter
|
|
@@ -6297,13 +6377,16 @@ ${textContent}` };
|
|
|
6297
6377
|
const usage = await result.usage;
|
|
6298
6378
|
const toolCallsResult = await result.toolCalls;
|
|
6299
6379
|
const stepCachedTokens = usage.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
6300
|
-
|
|
6380
|
+
const stepInputTokens = usage.inputTokens ?? 0;
|
|
6381
|
+
totalInputTokens += stepInputTokens;
|
|
6301
6382
|
totalOutputTokens += usage.outputTokens ?? 0;
|
|
6302
6383
|
totalCachedTokens += stepCachedTokens;
|
|
6384
|
+
latestContextTokens = stepInputTokens;
|
|
6385
|
+
toolOutputEstimateSinceModel = 0;
|
|
6303
6386
|
yield pushEvent({
|
|
6304
6387
|
type: "model:response",
|
|
6305
6388
|
usage: {
|
|
6306
|
-
input:
|
|
6389
|
+
input: stepInputTokens,
|
|
6307
6390
|
output: usage.outputTokens ?? 0,
|
|
6308
6391
|
cached: stepCachedTokens
|
|
6309
6392
|
}
|
|
@@ -6349,7 +6432,9 @@ ${textContent}` };
|
|
|
6349
6432
|
output: totalOutputTokens,
|
|
6350
6433
|
cached: totalCachedTokens
|
|
6351
6434
|
},
|
|
6352
|
-
duration: now() - start
|
|
6435
|
+
duration: now() - start,
|
|
6436
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
6437
|
+
contextWindow
|
|
6353
6438
|
};
|
|
6354
6439
|
yield pushEvent({ type: "run:completed", runId, result: result2 });
|
|
6355
6440
|
return;
|
|
@@ -6477,6 +6562,7 @@ ${textContent}` };
|
|
|
6477
6562
|
span?.end({ result: { value: result2.output ?? null, isError: false } });
|
|
6478
6563
|
const serialized = JSON.stringify(result2.output ?? null);
|
|
6479
6564
|
const outputTokenEstimate = Math.ceil(serialized.length / 4);
|
|
6565
|
+
toolOutputEstimateSinceModel += outputTokenEstimate;
|
|
6480
6566
|
yield pushEvent({
|
|
6481
6567
|
type: "tool:completed",
|
|
6482
6568
|
tool: result2.tool,
|
|
@@ -6587,7 +6673,9 @@ ${this.skillFingerprint}`;
|
|
|
6587
6673
|
duration: now() - start,
|
|
6588
6674
|
continuation: true,
|
|
6589
6675
|
continuationMessages: [...messages],
|
|
6590
|
-
maxSteps
|
|
6676
|
+
maxSteps,
|
|
6677
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
6678
|
+
contextWindow
|
|
6591
6679
|
};
|
|
6592
6680
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
6593
6681
|
} else {
|
|
@@ -6915,6 +7003,13 @@ var InMemoryConversationStore = class {
|
|
|
6915
7003
|
conversation.pendingSubagentResults.push(result);
|
|
6916
7004
|
conversation.updatedAt = Date.now();
|
|
6917
7005
|
}
|
|
7006
|
+
async clearCallbackLock(conversationId) {
|
|
7007
|
+
const conversation = this.conversations.get(conversationId);
|
|
7008
|
+
if (!conversation) return void 0;
|
|
7009
|
+
conversation.runningCallbackSince = void 0;
|
|
7010
|
+
conversation.updatedAt = Date.now();
|
|
7011
|
+
return conversation;
|
|
7012
|
+
}
|
|
6918
7013
|
};
|
|
6919
7014
|
var FileConversationStore = class {
|
|
6920
7015
|
workingDir;
|
|
@@ -7137,6 +7232,28 @@ var FileConversationStore = class {
|
|
|
7137
7232
|
conversation.updatedAt = Date.now();
|
|
7138
7233
|
await this.update(conversation);
|
|
7139
7234
|
}
|
|
7235
|
+
async clearCallbackLock(conversationId) {
|
|
7236
|
+
await this.ensureLoaded();
|
|
7237
|
+
const summary = this.conversations.get(conversationId);
|
|
7238
|
+
if (!summary) return void 0;
|
|
7239
|
+
const { conversationsDir } = await this.resolvePaths();
|
|
7240
|
+
const filePath = resolve11(conversationsDir, summary.fileName);
|
|
7241
|
+
let result;
|
|
7242
|
+
this.writing = this.writing.then(async () => {
|
|
7243
|
+
const conv = await this.readConversationFile(summary.fileName);
|
|
7244
|
+
if (!conv) return;
|
|
7245
|
+
conv.runningCallbackSince = void 0;
|
|
7246
|
+
conv.updatedAt = Date.now();
|
|
7247
|
+
await writeJsonAtomic3(filePath, conv);
|
|
7248
|
+
this.conversations.set(conversationId, {
|
|
7249
|
+
...summary,
|
|
7250
|
+
updatedAt: conv.updatedAt
|
|
7251
|
+
});
|
|
7252
|
+
result = conv;
|
|
7253
|
+
});
|
|
7254
|
+
await this.writing;
|
|
7255
|
+
return result;
|
|
7256
|
+
}
|
|
7140
7257
|
};
|
|
7141
7258
|
var FileStateStore = class {
|
|
7142
7259
|
workingDir;
|
|
@@ -7477,6 +7594,18 @@ var KeyValueConversationStoreBase = class {
|
|
|
7477
7594
|
await this.update(conversation);
|
|
7478
7595
|
});
|
|
7479
7596
|
}
|
|
7597
|
+
async clearCallbackLock(conversationId) {
|
|
7598
|
+
let result;
|
|
7599
|
+
await this.withAppendLock(conversationId, async () => {
|
|
7600
|
+
const conversation = await this.get(conversationId);
|
|
7601
|
+
if (!conversation) return;
|
|
7602
|
+
conversation.runningCallbackSince = void 0;
|
|
7603
|
+
conversation.updatedAt = Date.now();
|
|
7604
|
+
await this.update(conversation);
|
|
7605
|
+
result = conversation;
|
|
7606
|
+
});
|
|
7607
|
+
return result;
|
|
7608
|
+
}
|
|
7480
7609
|
};
|
|
7481
7610
|
var UpstashConversationStore = class extends KeyValueConversationStoreBase {
|
|
7482
7611
|
baseUrl;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"redis": "^5.10.0",
|
|
33
33
|
"yaml": "^2.4.0",
|
|
34
34
|
"zod": "^3.22.0",
|
|
35
|
-
"@poncho-ai/sdk": "1.6.
|
|
35
|
+
"@poncho-ai/sdk": "1.6.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/mustache": "^4.2.6",
|
package/src/harness.ts
CHANGED
|
@@ -1554,6 +1554,8 @@ ${boundedMainMemory.trim()}`
|
|
|
1554
1554
|
let totalOutputTokens = 0;
|
|
1555
1555
|
let totalCachedTokens = 0;
|
|
1556
1556
|
let transientStepRetryCount = 0;
|
|
1557
|
+
let latestContextTokens = 0;
|
|
1558
|
+
let toolOutputEstimateSinceModel = 0;
|
|
1557
1559
|
let cachedCoreMessages: ModelMessage[] = [];
|
|
1558
1560
|
let convertedUpTo = 0;
|
|
1559
1561
|
|
|
@@ -1585,6 +1587,8 @@ ${boundedMainMemory.trim()}`
|
|
|
1585
1587
|
continuation: true,
|
|
1586
1588
|
continuationMessages: [...messages],
|
|
1587
1589
|
maxSteps,
|
|
1590
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
1591
|
+
contextWindow,
|
|
1588
1592
|
};
|
|
1589
1593
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
1590
1594
|
return;
|
|
@@ -1885,10 +1889,13 @@ ${boundedMainMemory.trim()}`
|
|
|
1885
1889
|
if (emittedMessages.length > 0 && emittedMessages[emittedMessages.length - 1].role === "user") {
|
|
1886
1890
|
emittedMessages.pop();
|
|
1887
1891
|
}
|
|
1892
|
+
const tokensAfterCompaction = estimateTotalTokens(integrityPrompt, messages, toolDefsJson);
|
|
1893
|
+
latestContextTokens = tokensAfterCompaction;
|
|
1894
|
+
toolOutputEstimateSinceModel = 0;
|
|
1888
1895
|
yield pushEvent({
|
|
1889
1896
|
type: "compaction:completed",
|
|
1890
1897
|
tokensBefore: effectiveTokens,
|
|
1891
|
-
tokensAfter:
|
|
1898
|
+
tokensAfter: tokensAfterCompaction,
|
|
1892
1899
|
messagesBefore: compactResult.messagesBefore!,
|
|
1893
1900
|
compactedMessages: emittedMessages,
|
|
1894
1901
|
messagesAfter: compactResult.messagesAfter!,
|
|
@@ -2060,14 +2067,17 @@ ${boundedMainMemory.trim()}`
|
|
|
2060
2067
|
|
|
2061
2068
|
// Update token usage
|
|
2062
2069
|
const stepCachedTokens = usage.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
2063
|
-
|
|
2070
|
+
const stepInputTokens = usage.inputTokens ?? 0;
|
|
2071
|
+
totalInputTokens += stepInputTokens;
|
|
2064
2072
|
totalOutputTokens += usage.outputTokens ?? 0;
|
|
2065
2073
|
totalCachedTokens += stepCachedTokens;
|
|
2074
|
+
latestContextTokens = stepInputTokens;
|
|
2075
|
+
toolOutputEstimateSinceModel = 0;
|
|
2066
2076
|
|
|
2067
2077
|
yield pushEvent({
|
|
2068
2078
|
type: "model:response",
|
|
2069
2079
|
usage: {
|
|
2070
|
-
input:
|
|
2080
|
+
input: stepInputTokens,
|
|
2071
2081
|
output: usage.outputTokens ?? 0,
|
|
2072
2082
|
cached: stepCachedTokens,
|
|
2073
2083
|
},
|
|
@@ -2120,6 +2130,8 @@ ${boundedMainMemory.trim()}`
|
|
|
2120
2130
|
cached: totalCachedTokens,
|
|
2121
2131
|
},
|
|
2122
2132
|
duration: now() - start,
|
|
2133
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
2134
|
+
contextWindow,
|
|
2123
2135
|
};
|
|
2124
2136
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
2125
2137
|
return;
|
|
@@ -2290,6 +2302,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2290
2302
|
span?.end({ result: { value: result.output ?? null, isError: false } });
|
|
2291
2303
|
const serialized = JSON.stringify(result.output ?? null);
|
|
2292
2304
|
const outputTokenEstimate = Math.ceil(serialized.length / 4);
|
|
2305
|
+
toolOutputEstimateSinceModel += outputTokenEstimate;
|
|
2293
2306
|
yield pushEvent({
|
|
2294
2307
|
type: "tool:completed",
|
|
2295
2308
|
tool: result.tool,
|
|
@@ -2415,6 +2428,8 @@ ${boundedMainMemory.trim()}`
|
|
|
2415
2428
|
continuation: true,
|
|
2416
2429
|
continuationMessages: [...messages],
|
|
2417
2430
|
maxSteps,
|
|
2431
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
2432
|
+
contextWindow,
|
|
2418
2433
|
};
|
|
2419
2434
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
2420
2435
|
} else {
|
package/src/search-tools.ts
CHANGED
|
@@ -6,6 +6,13 @@ const SEARCH_UA =
|
|
|
6
6
|
|
|
7
7
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
8
8
|
|
|
9
|
+
const SEARCH_MAX_RETRIES = 4;
|
|
10
|
+
const SEARCH_INITIAL_DELAY_MS = 2_000;
|
|
11
|
+
const SEARCH_MIN_INTERVAL_MS = 4_000;
|
|
12
|
+
const SEARCH_FALLBACK_COOLDOWN_MS = 12_000;
|
|
13
|
+
|
|
14
|
+
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
15
|
+
|
|
9
16
|
// ---------------------------------------------------------------------------
|
|
10
17
|
// web_search — Brave Search HTML scraping (no API key)
|
|
11
18
|
// ---------------------------------------------------------------------------
|
|
@@ -16,21 +23,100 @@ interface SearchResult {
|
|
|
16
23
|
snippet: string;
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
let searchQueue: Promise<void> = Promise.resolve();
|
|
27
|
+
let nextSearchAllowedAt = 0;
|
|
28
|
+
|
|
29
|
+
function parseRetryAfterMs(retryAfterHeader: string | null): number {
|
|
30
|
+
if (!retryAfterHeader) return SEARCH_FALLBACK_COOLDOWN_MS;
|
|
31
|
+
const asSeconds = Number(retryAfterHeader);
|
|
32
|
+
if (Number.isFinite(asSeconds) && asSeconds >= 0) {
|
|
33
|
+
return Math.max(Math.floor(asSeconds * 1000), SEARCH_MIN_INTERVAL_MS);
|
|
34
|
+
}
|
|
35
|
+
const asDate = new Date(retryAfterHeader).getTime();
|
|
36
|
+
if (Number.isFinite(asDate)) {
|
|
37
|
+
return Math.max(asDate - Date.now(), SEARCH_MIN_INTERVAL_MS);
|
|
38
|
+
}
|
|
39
|
+
return SEARCH_FALLBACK_COOLDOWN_MS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applyRateLimitCooldown(retryAfterHeader: string | null): void {
|
|
43
|
+
const cooldownMs = parseRetryAfterMs(retryAfterHeader);
|
|
44
|
+
nextSearchAllowedAt = Math.max(nextSearchAllowedAt, Date.now() + cooldownMs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runWithSearchThrottle<T>(fn: () => Promise<T>): Promise<T> {
|
|
48
|
+
const previous = searchQueue;
|
|
49
|
+
let release: (() => void) | undefined;
|
|
50
|
+
searchQueue = new Promise<void>((resolve) => {
|
|
51
|
+
release = resolve;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await previous.catch(() => {});
|
|
55
|
+
try {
|
|
56
|
+
const waitMs = nextSearchAllowedAt - Date.now();
|
|
57
|
+
if (waitMs > 0) {
|
|
58
|
+
await sleep(waitMs);
|
|
59
|
+
}
|
|
60
|
+
const result = await fn();
|
|
61
|
+
nextSearchAllowedAt = Math.max(nextSearchAllowedAt, Date.now() + SEARCH_MIN_INTERVAL_MS);
|
|
62
|
+
return result;
|
|
63
|
+
} finally {
|
|
64
|
+
release?.();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isRetryableStatus(status: number): boolean {
|
|
69
|
+
return status === 429 || status === 503 || status >= 500;
|
|
70
|
+
}
|
|
71
|
+
|
|
19
72
|
async function braveSearch(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
20
73
|
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
74
|
+
|
|
75
|
+
let lastError: Error | undefined;
|
|
76
|
+
for (let attempt = 0; attempt < SEARCH_MAX_RETRIES; attempt++) {
|
|
77
|
+
if (attempt > 0) {
|
|
78
|
+
const delay = SEARCH_INITIAL_DELAY_MS * 2 ** (attempt - 1) + Math.floor(Math.random() * 500);
|
|
79
|
+
await sleep(delay);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const html = await runWithSearchThrottle(async () => {
|
|
84
|
+
const res = await fetch(url, {
|
|
85
|
+
headers: {
|
|
86
|
+
"User-Agent": SEARCH_UA,
|
|
87
|
+
Accept: "text/html,application/xhtml+xml",
|
|
88
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
89
|
+
},
|
|
90
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
if (res.status === 429) {
|
|
95
|
+
applyRateLimitCooldown(res.headers.get("retry-after"));
|
|
96
|
+
}
|
|
97
|
+
const error = new Error(`Search request failed (${res.status} ${res.statusText})`);
|
|
98
|
+
if (isRetryableStatus(res.status)) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
// Non-retryable status: surface immediately.
|
|
102
|
+
(error as Error & { retryable?: boolean }).retryable = false;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return await res.text();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return parseBraveResults(html, maxResults);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
112
|
+
if ((lastError as Error & { retryable?: boolean }).retryable === false) {
|
|
113
|
+
throw lastError;
|
|
114
|
+
}
|
|
115
|
+
if (attempt < SEARCH_MAX_RETRIES - 1) continue;
|
|
116
|
+
}
|
|
31
117
|
}
|
|
32
|
-
|
|
33
|
-
|
|
118
|
+
|
|
119
|
+
throw lastError ?? new Error("Search failed after retries");
|
|
34
120
|
}
|
|
35
121
|
|
|
36
122
|
function parseBraveResults(html: string, max: number): SearchResult[] {
|
package/src/skill-tools.ts
CHANGED
|
@@ -412,17 +412,19 @@ const loadRunnableScriptFunction = async (
|
|
|
412
412
|
|
|
413
413
|
const loadScriptModule = async (scriptPath: string): Promise<unknown> => {
|
|
414
414
|
const extension = extname(scriptPath).toLowerCase();
|
|
415
|
-
// Node
|
|
416
|
-
//
|
|
415
|
+
// Both Node's native import() and jiti cache modules by URL/path.
|
|
416
|
+
// Append a cache-busting query string so edits made by the agent are
|
|
417
|
+
// picked up on the next run_skill_script call.
|
|
418
|
+
const cacheBust = `?t=${Date.now()}`;
|
|
417
419
|
if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
|
|
418
|
-
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
419
|
-
return await jiti.import(scriptPath);
|
|
420
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
421
|
+
return await jiti.import(scriptPath + cacheBust);
|
|
420
422
|
}
|
|
421
423
|
try {
|
|
422
|
-
return await import(pathToFileURL(scriptPath).href);
|
|
424
|
+
return await import(pathToFileURL(scriptPath).href + cacheBust);
|
|
423
425
|
} catch {
|
|
424
|
-
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
425
|
-
return await jiti.import(scriptPath);
|
|
426
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
427
|
+
return await jiti.import(scriptPath + cacheBust);
|
|
426
428
|
}
|
|
427
429
|
};
|
|
428
430
|
|
package/src/state.ts
CHANGED
|
@@ -84,6 +84,12 @@ export interface ConversationStore {
|
|
|
84
84
|
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
85
85
|
delete(conversationId: string): Promise<boolean>;
|
|
86
86
|
appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Atomically clear `runningCallbackSince` without clobbering other fields.
|
|
89
|
+
* Returns the conversation as it exists after the clear (with current
|
|
90
|
+
* `pendingSubagentResults`).
|
|
91
|
+
*/
|
|
92
|
+
clearCallbackLock(conversationId: string): Promise<Conversation | undefined>;
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
export type StateProviderName =
|
|
@@ -325,6 +331,14 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
325
331
|
conversation.pendingSubagentResults.push(result);
|
|
326
332
|
conversation.updatedAt = Date.now();
|
|
327
333
|
}
|
|
334
|
+
|
|
335
|
+
async clearCallbackLock(conversationId: string): Promise<Conversation | undefined> {
|
|
336
|
+
const conversation = this.conversations.get(conversationId);
|
|
337
|
+
if (!conversation) return undefined;
|
|
338
|
+
conversation.runningCallbackSince = undefined;
|
|
339
|
+
conversation.updatedAt = Date.now();
|
|
340
|
+
return conversation;
|
|
341
|
+
}
|
|
328
342
|
}
|
|
329
343
|
|
|
330
344
|
export type ConversationSummary = {
|
|
@@ -607,6 +621,31 @@ class FileConversationStore implements ConversationStore {
|
|
|
607
621
|
conversation.updatedAt = Date.now();
|
|
608
622
|
await this.update(conversation);
|
|
609
623
|
}
|
|
624
|
+
|
|
625
|
+
async clearCallbackLock(conversationId: string): Promise<Conversation | undefined> {
|
|
626
|
+
await this.ensureLoaded();
|
|
627
|
+
const summary = this.conversations.get(conversationId);
|
|
628
|
+
if (!summary) return undefined;
|
|
629
|
+
const { conversationsDir } = await this.resolvePaths();
|
|
630
|
+
const filePath = resolve(conversationsDir, summary.fileName);
|
|
631
|
+
let result: Conversation | undefined;
|
|
632
|
+
// Read inside the writing chain so we see the latest state after any
|
|
633
|
+
// pending appendSubagentResult writes have flushed.
|
|
634
|
+
this.writing = this.writing.then(async () => {
|
|
635
|
+
const conv = await this.readConversationFile(summary.fileName);
|
|
636
|
+
if (!conv) return;
|
|
637
|
+
conv.runningCallbackSince = undefined;
|
|
638
|
+
conv.updatedAt = Date.now();
|
|
639
|
+
await writeJsonAtomic(filePath, conv);
|
|
640
|
+
this.conversations.set(conversationId, {
|
|
641
|
+
...summary,
|
|
642
|
+
updatedAt: conv.updatedAt,
|
|
643
|
+
});
|
|
644
|
+
result = conv;
|
|
645
|
+
});
|
|
646
|
+
await this.writing;
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
610
649
|
}
|
|
611
650
|
|
|
612
651
|
type LocalStateFile = {
|
|
@@ -1005,6 +1044,19 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
1005
1044
|
await this.update(conversation);
|
|
1006
1045
|
});
|
|
1007
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
async clearCallbackLock(conversationId: string): Promise<Conversation | undefined> {
|
|
1049
|
+
let result: Conversation | undefined;
|
|
1050
|
+
await this.withAppendLock(conversationId, async () => {
|
|
1051
|
+
const conversation = await this.get(conversationId);
|
|
1052
|
+
if (!conversation) return;
|
|
1053
|
+
conversation.runningCallbackSince = undefined;
|
|
1054
|
+
conversation.updatedAt = Date.now();
|
|
1055
|
+
await this.update(conversation);
|
|
1056
|
+
result = conversation;
|
|
1057
|
+
});
|
|
1058
|
+
return result;
|
|
1059
|
+
}
|
|
1008
1060
|
}
|
|
1009
1061
|
|
|
1010
1062
|
class UpstashConversationStore extends KeyValueConversationStoreBase {
|