@poncho-ai/harness 0.27.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 +17 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +294 -19
- package/package.json +3 -2
- package/src/config.ts +2 -0
- package/src/harness.ts +30 -5
- package/src/index.ts +1 -0
- package/src/search-tools.ts +267 -0
- 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.
|
|
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,22 @@
|
|
|
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
|
+
|
|
14
|
+
## 0.28.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- [`c0ca56b`](https://github.com/cesr/poncho-ai/commit/c0ca56b54bb877d96ba8088537d6f1c7461d2a55) Thanks [@cesr](https://github.com/cesr)! - Add built-in `web_search` and `web_fetch` tools so agents can search the web and fetch page content without a browser or API keys. Remove the scaffolded `fetch-page` skill (superseded by `web_fetch`). Fix `browser_open` crash when agent projects have an older `@poncho-ai/browser` installed.
|
|
19
|
+
|
|
3
20
|
## 0.27.0
|
|
4
21
|
|
|
5
22
|
### 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;
|
|
@@ -351,6 +358,8 @@ type BuiltInToolToggles = {
|
|
|
351
358
|
todo_add?: boolean;
|
|
352
359
|
todo_update?: boolean;
|
|
353
360
|
todo_remove?: boolean;
|
|
361
|
+
web_search?: boolean;
|
|
362
|
+
web_fetch?: boolean;
|
|
354
363
|
};
|
|
355
364
|
interface MessagingChannelConfig {
|
|
356
365
|
platform: "slack" | "resend" | "telegram";
|
|
@@ -738,6 +747,8 @@ declare class LatitudeCapture {
|
|
|
738
747
|
*/
|
|
739
748
|
declare function jsonSchemaToZod(schema: JsonSchema): z.ZodType;
|
|
740
749
|
|
|
750
|
+
declare const createSearchTools: () => ToolDefinition[];
|
|
751
|
+
|
|
741
752
|
/**
|
|
742
753
|
* Resolve the full list of skill directories to scan.
|
|
743
754
|
* Merges the defaults with any extra paths provided via config.
|
|
@@ -813,4 +824,4 @@ declare class TelemetryEmitter {
|
|
|
813
824
|
|
|
814
825
|
declare const createSubagentTools: (manager: SubagentManager) => ToolDefinition[];
|
|
815
826
|
|
|
816
|
-
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentResult, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
|
827
|
+
export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentResult, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, compactMessages, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryTools, createModelProvider, createSearchTools, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, findSafeSplitPoint, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, ponchoDocsTool, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
|
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) => {
|
|
@@ -4258,10 +4259,218 @@ var extractRunnableFunction = (value) => {
|
|
|
4258
4259
|
return void 0;
|
|
4259
4260
|
};
|
|
4260
4261
|
|
|
4261
|
-
// src/
|
|
4262
|
+
// src/search-tools.ts
|
|
4263
|
+
import { load as cheerioLoad } from "cheerio";
|
|
4262
4264
|
import { defineTool as defineTool5 } from "@poncho-ai/sdk";
|
|
4263
|
-
var
|
|
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";
|
|
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
|
+
}
|
|
4313
|
+
async function braveSearch(query, maxResults) {
|
|
4314
|
+
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
|
|
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
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
throw lastError ?? new Error("Search failed after retries");
|
|
4354
|
+
}
|
|
4355
|
+
function parseBraveResults(html, max) {
|
|
4356
|
+
const $ = cheerioLoad(html);
|
|
4357
|
+
const results = [];
|
|
4358
|
+
$('div.snippet[data-type="web"]').each((_i, el) => {
|
|
4359
|
+
if (results.length >= max) return false;
|
|
4360
|
+
const $el = $(el);
|
|
4361
|
+
const anchor = $el.find(".result-content a").first();
|
|
4362
|
+
const href = anchor.attr("href") ?? "";
|
|
4363
|
+
if (!href.startsWith("http")) return;
|
|
4364
|
+
const title = $el.find(".title").first().text().trim();
|
|
4365
|
+
const snippet = $el.find(".generic-snippet .content").first().text().trim();
|
|
4366
|
+
if (title) {
|
|
4367
|
+
results.push({ title, url: href, snippet });
|
|
4368
|
+
}
|
|
4369
|
+
});
|
|
4370
|
+
return results;
|
|
4371
|
+
}
|
|
4372
|
+
var DEFAULT_MAX_LENGTH = 16e3;
|
|
4373
|
+
function extractReadableText($, maxLength) {
|
|
4374
|
+
const title = $("title").first().text().trim();
|
|
4375
|
+
$("script, style, noscript, nav, footer, header, aside, [role='navigation'], [role='banner'], [role='contentinfo']").remove();
|
|
4376
|
+
$("svg, iframe, form, button, input, select, textarea").remove();
|
|
4377
|
+
let root = $("article").first();
|
|
4378
|
+
if (!root.length) root = $("main").first();
|
|
4379
|
+
if (!root.length) root = $("[role='main']").first();
|
|
4380
|
+
if (!root.length) root = $("body").first();
|
|
4381
|
+
const text = root.text().replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
4382
|
+
const content = text.length > maxLength ? text.slice(0, maxLength) + "\n\u2026(truncated)" : text;
|
|
4383
|
+
return { title, content };
|
|
4384
|
+
}
|
|
4385
|
+
var createSearchTools = () => [
|
|
4386
|
+
defineTool5({
|
|
4387
|
+
name: "web_search",
|
|
4388
|
+
description: "Search the web and return a list of results (title, URL, snippet). Use this instead of opening a browser when you need to find information online.",
|
|
4389
|
+
inputSchema: {
|
|
4390
|
+
type: "object",
|
|
4391
|
+
properties: {
|
|
4392
|
+
query: {
|
|
4393
|
+
type: "string",
|
|
4394
|
+
description: "The search query"
|
|
4395
|
+
},
|
|
4396
|
+
max_results: {
|
|
4397
|
+
type: "number",
|
|
4398
|
+
description: "Maximum number of results to return (1-10, default 5)"
|
|
4399
|
+
}
|
|
4400
|
+
},
|
|
4401
|
+
required: ["query"],
|
|
4402
|
+
additionalProperties: false
|
|
4403
|
+
},
|
|
4404
|
+
handler: async (input) => {
|
|
4405
|
+
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
4406
|
+
if (!query) {
|
|
4407
|
+
return { error: "A non-empty query string is required." };
|
|
4408
|
+
}
|
|
4409
|
+
const max = Math.min(Math.max(Number(input.max_results) || 5, 1), 10);
|
|
4410
|
+
try {
|
|
4411
|
+
const results = await braveSearch(query, max);
|
|
4412
|
+
if (results.length === 0) {
|
|
4413
|
+
return { query, results: [], note: "No results found. Try rephrasing your query." };
|
|
4414
|
+
}
|
|
4415
|
+
return { query, results };
|
|
4416
|
+
} catch (err) {
|
|
4417
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4418
|
+
return {
|
|
4419
|
+
error: `Search failed: ${msg}`,
|
|
4420
|
+
hint: "The search provider may be rate-limiting requests. Try again shortly, or use browser tools as a fallback."
|
|
4421
|
+
};
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
}),
|
|
4264
4425
|
defineTool5({
|
|
4426
|
+
name: "web_fetch",
|
|
4427
|
+
description: "Fetch a web page and return its text content (HTML tags stripped). Useful for reading articles, documentation, or any web page without opening a browser.",
|
|
4428
|
+
inputSchema: {
|
|
4429
|
+
type: "object",
|
|
4430
|
+
properties: {
|
|
4431
|
+
url: {
|
|
4432
|
+
type: "string",
|
|
4433
|
+
description: "The URL to fetch"
|
|
4434
|
+
},
|
|
4435
|
+
max_length: {
|
|
4436
|
+
type: "number",
|
|
4437
|
+
description: `Maximum character length of returned content (default ${DEFAULT_MAX_LENGTH})`
|
|
4438
|
+
}
|
|
4439
|
+
},
|
|
4440
|
+
required: ["url"],
|
|
4441
|
+
additionalProperties: false
|
|
4442
|
+
},
|
|
4443
|
+
handler: async (input) => {
|
|
4444
|
+
const url = typeof input.url === "string" ? input.url.trim() : "";
|
|
4445
|
+
if (!url) {
|
|
4446
|
+
return { error: 'A "url" string is required.' };
|
|
4447
|
+
}
|
|
4448
|
+
const maxLength = Math.max(Number(input.max_length) || DEFAULT_MAX_LENGTH, 1e3);
|
|
4449
|
+
try {
|
|
4450
|
+
const res = await fetch(url, {
|
|
4451
|
+
headers: { "User-Agent": SEARCH_UA, Accept: "text/html,application/xhtml+xml" },
|
|
4452
|
+
redirect: "follow",
|
|
4453
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
4454
|
+
});
|
|
4455
|
+
if (!res.ok) {
|
|
4456
|
+
return { url, status: res.status, error: res.statusText };
|
|
4457
|
+
}
|
|
4458
|
+
const html = await res.text();
|
|
4459
|
+
const $ = cheerioLoad(html);
|
|
4460
|
+
const { title, content } = extractReadableText($, maxLength);
|
|
4461
|
+
return { url, status: res.status, title, content };
|
|
4462
|
+
} catch (err) {
|
|
4463
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4464
|
+
return { url, error: `Fetch failed: ${msg}` };
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
})
|
|
4468
|
+
];
|
|
4469
|
+
|
|
4470
|
+
// src/subagent-tools.ts
|
|
4471
|
+
import { defineTool as defineTool6 } from "@poncho-ai/sdk";
|
|
4472
|
+
var createSubagentTools = (manager) => [
|
|
4473
|
+
defineTool6({
|
|
4265
4474
|
name: "spawn_subagent",
|
|
4266
4475
|
description: "Spawn a subagent to work on a task in the background. Returns immediately with a subagent ID. The subagent runs independently and its result will be delivered to you as a message in the conversation when it completes.\n\nGuidelines:\n- Spawn all needed subagents in a SINGLE response (they run concurrently), then end your turn with a brief message to the user.\n- Do NOT spawn more subagents in follow-up steps. Wait for results to be delivered before deciding if more work is needed.\n- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
|
|
4267
4476
|
inputSchema: {
|
|
@@ -4293,7 +4502,7 @@ var createSubagentTools = (manager) => [
|
|
|
4293
4502
|
return { subagentId, status: "running" };
|
|
4294
4503
|
}
|
|
4295
4504
|
}),
|
|
4296
|
-
|
|
4505
|
+
defineTool6({
|
|
4297
4506
|
name: "message_subagent",
|
|
4298
4507
|
description: "Send a follow-up message to a completed or stopped subagent. The subagent restarts in the background and its result will be delivered to you as a message when it completes. Only works when the subagent is not currently running.",
|
|
4299
4508
|
inputSchema: {
|
|
@@ -4321,7 +4530,7 @@ var createSubagentTools = (manager) => [
|
|
|
4321
4530
|
return { subagentId: id, status: "running" };
|
|
4322
4531
|
}
|
|
4323
4532
|
}),
|
|
4324
|
-
|
|
4533
|
+
defineTool6({
|
|
4325
4534
|
name: "stop_subagent",
|
|
4326
4535
|
description: "Stop a running subagent. The subagent's conversation is preserved but it will stop processing. Use this to cancel work that is no longer needed.",
|
|
4327
4536
|
inputSchema: {
|
|
@@ -4344,7 +4553,7 @@ var createSubagentTools = (manager) => [
|
|
|
4344
4553
|
return { message: `Subagent "${subagentId}" has been stopped.` };
|
|
4345
4554
|
}
|
|
4346
4555
|
}),
|
|
4347
|
-
|
|
4556
|
+
defineTool6({
|
|
4348
4557
|
name: "list_subagents",
|
|
4349
4558
|
description: "List all subagents that have been spawned in this conversation. Returns each subagent's ID, original task, current status, and message count. Use this to look up subagent IDs before calling message_subagent or stop_subagent.",
|
|
4350
4559
|
inputSchema: {
|
|
@@ -4970,6 +5179,11 @@ var AgentHarness = class _AgentHarness {
|
|
|
4970
5179
|
if (this.isToolEnabled("delete_directory")) {
|
|
4971
5180
|
this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
|
|
4972
5181
|
}
|
|
5182
|
+
for (const tool of createSearchTools()) {
|
|
5183
|
+
if (this.isToolEnabled(tool.name)) {
|
|
5184
|
+
this.registerIfMissing(tool);
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
4973
5187
|
if (this.environment === "development" && this.isToolEnabled("poncho_docs")) {
|
|
4974
5188
|
this.registerIfMissing(ponchoDocsTool);
|
|
4975
5189
|
}
|
|
@@ -5455,7 +5669,11 @@ var AgentHarness = class _AgentHarness {
|
|
|
5455
5669
|
const session = new browserMod.BrowserSession(sessionId, browserCfg);
|
|
5456
5670
|
this._browserSession = session;
|
|
5457
5671
|
const tools = browserMod.createBrowserTools(
|
|
5458
|
-
() => session
|
|
5672
|
+
() => session,
|
|
5673
|
+
// Backward compat: older @poncho-ai/browser versions expect a second
|
|
5674
|
+
// getConversationId callback. Current versions read from ToolContext
|
|
5675
|
+
// and ignore extra args.
|
|
5676
|
+
() => "__default__"
|
|
5459
5677
|
);
|
|
5460
5678
|
for (const tool of tools) {
|
|
5461
5679
|
if (this.isToolEnabled(tool.name)) {
|
|
@@ -5733,6 +5951,8 @@ ${this.skillFingerprint}`;
|
|
|
5733
5951
|
let totalOutputTokens = 0;
|
|
5734
5952
|
let totalCachedTokens = 0;
|
|
5735
5953
|
let transientStepRetryCount = 0;
|
|
5954
|
+
let latestContextTokens = 0;
|
|
5955
|
+
let toolOutputEstimateSinceModel = 0;
|
|
5736
5956
|
let cachedCoreMessages = [];
|
|
5737
5957
|
let convertedUpTo = 0;
|
|
5738
5958
|
for (let step = 1; step <= maxSteps; step += 1) {
|
|
@@ -5762,7 +5982,9 @@ ${this.skillFingerprint}`;
|
|
|
5762
5982
|
duration: now() - start,
|
|
5763
5983
|
continuation: true,
|
|
5764
5984
|
continuationMessages: [...messages],
|
|
5765
|
-
maxSteps
|
|
5985
|
+
maxSteps,
|
|
5986
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
5987
|
+
contextWindow
|
|
5766
5988
|
};
|
|
5767
5989
|
yield pushEvent({ type: "run:completed", runId, result: result2 });
|
|
5768
5990
|
return;
|
|
@@ -6002,10 +6224,13 @@ ${textContent}` };
|
|
|
6002
6224
|
if (emittedMessages.length > 0 && emittedMessages[emittedMessages.length - 1].role === "user") {
|
|
6003
6225
|
emittedMessages.pop();
|
|
6004
6226
|
}
|
|
6227
|
+
const tokensAfterCompaction = estimateTotalTokens(integrityPrompt, messages, toolDefsJson);
|
|
6228
|
+
latestContextTokens = tokensAfterCompaction;
|
|
6229
|
+
toolOutputEstimateSinceModel = 0;
|
|
6005
6230
|
yield pushEvent({
|
|
6006
6231
|
type: "compaction:completed",
|
|
6007
6232
|
tokensBefore: effectiveTokens,
|
|
6008
|
-
tokensAfter:
|
|
6233
|
+
tokensAfter: tokensAfterCompaction,
|
|
6009
6234
|
messagesBefore: compactResult.messagesBefore,
|
|
6010
6235
|
compactedMessages: emittedMessages,
|
|
6011
6236
|
messagesAfter: compactResult.messagesAfter
|
|
@@ -6152,13 +6377,16 @@ ${textContent}` };
|
|
|
6152
6377
|
const usage = await result.usage;
|
|
6153
6378
|
const toolCallsResult = await result.toolCalls;
|
|
6154
6379
|
const stepCachedTokens = usage.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
6155
|
-
|
|
6380
|
+
const stepInputTokens = usage.inputTokens ?? 0;
|
|
6381
|
+
totalInputTokens += stepInputTokens;
|
|
6156
6382
|
totalOutputTokens += usage.outputTokens ?? 0;
|
|
6157
6383
|
totalCachedTokens += stepCachedTokens;
|
|
6384
|
+
latestContextTokens = stepInputTokens;
|
|
6385
|
+
toolOutputEstimateSinceModel = 0;
|
|
6158
6386
|
yield pushEvent({
|
|
6159
6387
|
type: "model:response",
|
|
6160
6388
|
usage: {
|
|
6161
|
-
input:
|
|
6389
|
+
input: stepInputTokens,
|
|
6162
6390
|
output: usage.outputTokens ?? 0,
|
|
6163
6391
|
cached: stepCachedTokens
|
|
6164
6392
|
}
|
|
@@ -6204,7 +6432,9 @@ ${textContent}` };
|
|
|
6204
6432
|
output: totalOutputTokens,
|
|
6205
6433
|
cached: totalCachedTokens
|
|
6206
6434
|
},
|
|
6207
|
-
duration: now() - start
|
|
6435
|
+
duration: now() - start,
|
|
6436
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
6437
|
+
contextWindow
|
|
6208
6438
|
};
|
|
6209
6439
|
yield pushEvent({ type: "run:completed", runId, result: result2 });
|
|
6210
6440
|
return;
|
|
@@ -6332,6 +6562,7 @@ ${textContent}` };
|
|
|
6332
6562
|
span?.end({ result: { value: result2.output ?? null, isError: false } });
|
|
6333
6563
|
const serialized = JSON.stringify(result2.output ?? null);
|
|
6334
6564
|
const outputTokenEstimate = Math.ceil(serialized.length / 4);
|
|
6565
|
+
toolOutputEstimateSinceModel += outputTokenEstimate;
|
|
6335
6566
|
yield pushEvent({
|
|
6336
6567
|
type: "tool:completed",
|
|
6337
6568
|
tool: result2.tool,
|
|
@@ -6442,7 +6673,9 @@ ${this.skillFingerprint}`;
|
|
|
6442
6673
|
duration: now() - start,
|
|
6443
6674
|
continuation: true,
|
|
6444
6675
|
continuationMessages: [...messages],
|
|
6445
|
-
maxSteps
|
|
6676
|
+
maxSteps,
|
|
6677
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
6678
|
+
contextWindow
|
|
6446
6679
|
};
|
|
6447
6680
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
6448
6681
|
} else {
|
|
@@ -6770,6 +7003,13 @@ var InMemoryConversationStore = class {
|
|
|
6770
7003
|
conversation.pendingSubagentResults.push(result);
|
|
6771
7004
|
conversation.updatedAt = Date.now();
|
|
6772
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
|
+
}
|
|
6773
7013
|
};
|
|
6774
7014
|
var FileConversationStore = class {
|
|
6775
7015
|
workingDir;
|
|
@@ -6992,6 +7232,28 @@ var FileConversationStore = class {
|
|
|
6992
7232
|
conversation.updatedAt = Date.now();
|
|
6993
7233
|
await this.update(conversation);
|
|
6994
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
|
+
}
|
|
6995
7257
|
};
|
|
6996
7258
|
var FileStateStore = class {
|
|
6997
7259
|
workingDir;
|
|
@@ -7332,6 +7594,18 @@ var KeyValueConversationStoreBase = class {
|
|
|
7332
7594
|
await this.update(conversation);
|
|
7333
7595
|
});
|
|
7334
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
|
+
}
|
|
7335
7609
|
};
|
|
7336
7610
|
var UpstashConversationStore = class extends KeyValueConversationStoreBase {
|
|
7337
7611
|
baseUrl;
|
|
@@ -7782,7 +8056,7 @@ var TelemetryEmitter = class {
|
|
|
7782
8056
|
};
|
|
7783
8057
|
|
|
7784
8058
|
// src/index.ts
|
|
7785
|
-
import { defineTool as
|
|
8059
|
+
import { defineTool as defineTool7 } from "@poncho-ai/sdk";
|
|
7786
8060
|
export {
|
|
7787
8061
|
AgentHarness,
|
|
7788
8062
|
InMemoryConversationStore,
|
|
@@ -7807,12 +8081,13 @@ export {
|
|
|
7807
8081
|
createMemoryStore,
|
|
7808
8082
|
createMemoryTools,
|
|
7809
8083
|
createModelProvider,
|
|
8084
|
+
createSearchTools,
|
|
7810
8085
|
createSkillTools,
|
|
7811
8086
|
createStateStore,
|
|
7812
8087
|
createSubagentTools,
|
|
7813
8088
|
createUploadStore,
|
|
7814
8089
|
createWriteTool,
|
|
7815
|
-
|
|
8090
|
+
defineTool7 as defineTool,
|
|
7816
8091
|
deriveUploadKey,
|
|
7817
8092
|
ensureAgentIdentity,
|
|
7818
8093
|
estimateTokens,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
"@latitude-data/telemetry": "^2.0.4",
|
|
27
27
|
"@opentelemetry/api": "1.9.0",
|
|
28
28
|
"ai": "^6.0.86",
|
|
29
|
+
"cheerio": "^1.2.0",
|
|
29
30
|
"jiti": "^2.6.1",
|
|
30
31
|
"mustache": "^4.2.0",
|
|
31
32
|
"redis": "^5.10.0",
|
|
32
33
|
"yaml": "^2.4.0",
|
|
33
34
|
"zod": "^3.22.0",
|
|
34
|
-
"@poncho-ai/sdk": "1.6.
|
|
35
|
+
"@poncho-ai/sdk": "1.6.1"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/mustache": "^4.2.6",
|
package/src/config.ts
CHANGED
package/src/harness.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { addPromptCacheBreakpoints } from "./prompt-cache.js";
|
|
|
32
32
|
import { jsonSchemaToZod } from "./schema-converter.js";
|
|
33
33
|
import type { SkillMetadata } from "./skill-context.js";
|
|
34
34
|
import { createSkillTools, normalizeScriptPolicyPath } from "./skill-tools.js";
|
|
35
|
+
import { createSearchTools } from "./search-tools.js";
|
|
35
36
|
import { createSubagentTools } from "./subagent-tools.js";
|
|
36
37
|
import type { SubagentManager } from "./subagent-manager.js";
|
|
37
38
|
import { LatitudeTelemetry } from "@latitude-data/telemetry";
|
|
@@ -562,7 +563,7 @@ export class AgentHarness {
|
|
|
562
563
|
private insideTelemetryCapture = false;
|
|
563
564
|
private _browserSession?: unknown;
|
|
564
565
|
private _browserMod?: {
|
|
565
|
-
createBrowserTools: (getSession: () => unknown) => ToolDefinition[];
|
|
566
|
+
createBrowserTools: (getSession: () => unknown, getConversationId?: () => string) => ToolDefinition[];
|
|
566
567
|
BrowserSession: new (sessionId: string, config: Record<string, unknown>) => unknown;
|
|
567
568
|
};
|
|
568
569
|
|
|
@@ -644,6 +645,11 @@ export class AgentHarness {
|
|
|
644
645
|
if (this.isToolEnabled("delete_directory")) {
|
|
645
646
|
this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
|
|
646
647
|
}
|
|
648
|
+
for (const tool of createSearchTools()) {
|
|
649
|
+
if (this.isToolEnabled(tool.name)) {
|
|
650
|
+
this.registerIfMissing(tool);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
647
653
|
if (this.environment === "development" && this.isToolEnabled("poncho_docs")) {
|
|
648
654
|
this.registerIfMissing(ponchoDocsTool);
|
|
649
655
|
}
|
|
@@ -1161,7 +1167,7 @@ export class AgentHarness {
|
|
|
1161
1167
|
private async initBrowserTools(config: PonchoConfig): Promise<void> {
|
|
1162
1168
|
const spec = ["@poncho-ai", "browser"].join("/");
|
|
1163
1169
|
let browserMod: {
|
|
1164
|
-
createBrowserTools: (getSession: () => unknown) => ToolDefinition[];
|
|
1170
|
+
createBrowserTools: (getSession: () => unknown, getConversationId?: () => string) => ToolDefinition[];
|
|
1165
1171
|
BrowserSession: new (sessionId: string, cfg?: Record<string, unknown>) => unknown;
|
|
1166
1172
|
};
|
|
1167
1173
|
try {
|
|
@@ -1205,6 +1211,10 @@ export class AgentHarness {
|
|
|
1205
1211
|
|
|
1206
1212
|
const tools = browserMod.createBrowserTools(
|
|
1207
1213
|
() => session,
|
|
1214
|
+
// Backward compat: older @poncho-ai/browser versions expect a second
|
|
1215
|
+
// getConversationId callback. Current versions read from ToolContext
|
|
1216
|
+
// and ignore extra args.
|
|
1217
|
+
() => "__default__",
|
|
1208
1218
|
);
|
|
1209
1219
|
for (const tool of tools) {
|
|
1210
1220
|
if (this.isToolEnabled(tool.name)) {
|
|
@@ -1544,6 +1554,8 @@ ${boundedMainMemory.trim()}`
|
|
|
1544
1554
|
let totalOutputTokens = 0;
|
|
1545
1555
|
let totalCachedTokens = 0;
|
|
1546
1556
|
let transientStepRetryCount = 0;
|
|
1557
|
+
let latestContextTokens = 0;
|
|
1558
|
+
let toolOutputEstimateSinceModel = 0;
|
|
1547
1559
|
let cachedCoreMessages: ModelMessage[] = [];
|
|
1548
1560
|
let convertedUpTo = 0;
|
|
1549
1561
|
|
|
@@ -1575,6 +1587,8 @@ ${boundedMainMemory.trim()}`
|
|
|
1575
1587
|
continuation: true,
|
|
1576
1588
|
continuationMessages: [...messages],
|
|
1577
1589
|
maxSteps,
|
|
1590
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
1591
|
+
contextWindow,
|
|
1578
1592
|
};
|
|
1579
1593
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
1580
1594
|
return;
|
|
@@ -1875,10 +1889,13 @@ ${boundedMainMemory.trim()}`
|
|
|
1875
1889
|
if (emittedMessages.length > 0 && emittedMessages[emittedMessages.length - 1].role === "user") {
|
|
1876
1890
|
emittedMessages.pop();
|
|
1877
1891
|
}
|
|
1892
|
+
const tokensAfterCompaction = estimateTotalTokens(integrityPrompt, messages, toolDefsJson);
|
|
1893
|
+
latestContextTokens = tokensAfterCompaction;
|
|
1894
|
+
toolOutputEstimateSinceModel = 0;
|
|
1878
1895
|
yield pushEvent({
|
|
1879
1896
|
type: "compaction:completed",
|
|
1880
1897
|
tokensBefore: effectiveTokens,
|
|
1881
|
-
tokensAfter:
|
|
1898
|
+
tokensAfter: tokensAfterCompaction,
|
|
1882
1899
|
messagesBefore: compactResult.messagesBefore!,
|
|
1883
1900
|
compactedMessages: emittedMessages,
|
|
1884
1901
|
messagesAfter: compactResult.messagesAfter!,
|
|
@@ -2050,14 +2067,17 @@ ${boundedMainMemory.trim()}`
|
|
|
2050
2067
|
|
|
2051
2068
|
// Update token usage
|
|
2052
2069
|
const stepCachedTokens = usage.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
2053
|
-
|
|
2070
|
+
const stepInputTokens = usage.inputTokens ?? 0;
|
|
2071
|
+
totalInputTokens += stepInputTokens;
|
|
2054
2072
|
totalOutputTokens += usage.outputTokens ?? 0;
|
|
2055
2073
|
totalCachedTokens += stepCachedTokens;
|
|
2074
|
+
latestContextTokens = stepInputTokens;
|
|
2075
|
+
toolOutputEstimateSinceModel = 0;
|
|
2056
2076
|
|
|
2057
2077
|
yield pushEvent({
|
|
2058
2078
|
type: "model:response",
|
|
2059
2079
|
usage: {
|
|
2060
|
-
input:
|
|
2080
|
+
input: stepInputTokens,
|
|
2061
2081
|
output: usage.outputTokens ?? 0,
|
|
2062
2082
|
cached: stepCachedTokens,
|
|
2063
2083
|
},
|
|
@@ -2110,6 +2130,8 @@ ${boundedMainMemory.trim()}`
|
|
|
2110
2130
|
cached: totalCachedTokens,
|
|
2111
2131
|
},
|
|
2112
2132
|
duration: now() - start,
|
|
2133
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
2134
|
+
contextWindow,
|
|
2113
2135
|
};
|
|
2114
2136
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
2115
2137
|
return;
|
|
@@ -2280,6 +2302,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2280
2302
|
span?.end({ result: { value: result.output ?? null, isError: false } });
|
|
2281
2303
|
const serialized = JSON.stringify(result.output ?? null);
|
|
2282
2304
|
const outputTokenEstimate = Math.ceil(serialized.length / 4);
|
|
2305
|
+
toolOutputEstimateSinceModel += outputTokenEstimate;
|
|
2283
2306
|
yield pushEvent({
|
|
2284
2307
|
type: "tool:completed",
|
|
2285
2308
|
tool: result.tool,
|
|
@@ -2405,6 +2428,8 @@ ${boundedMainMemory.trim()}`
|
|
|
2405
2428
|
continuation: true,
|
|
2406
2429
|
continuationMessages: [...messages],
|
|
2407
2430
|
maxSteps,
|
|
2431
|
+
contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
|
|
2432
|
+
contextWindow,
|
|
2408
2433
|
};
|
|
2409
2434
|
yield pushEvent({ type: "run:completed", runId, result });
|
|
2410
2435
|
} else {
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./memory.js";
|
|
|
9
9
|
export * from "./mcp.js";
|
|
10
10
|
export * from "./model-factory.js";
|
|
11
11
|
export * from "./schema-converter.js";
|
|
12
|
+
export * from "./search-tools.js";
|
|
12
13
|
export * from "./skill-context.js";
|
|
13
14
|
export * from "./skill-tools.js";
|
|
14
15
|
export * from "./state.js";
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { load as cheerioLoad, type CheerioAPI } from "cheerio";
|
|
2
|
+
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
3
|
+
|
|
4
|
+
const SEARCH_UA =
|
|
5
|
+
"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";
|
|
6
|
+
|
|
7
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
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
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// web_search — Brave Search HTML scraping (no API key)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
interface SearchResult {
|
|
21
|
+
title: string;
|
|
22
|
+
url: string;
|
|
23
|
+
snippet: string;
|
|
24
|
+
}
|
|
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
|
+
|
|
72
|
+
async function braveSearch(query: string, maxResults: number): Promise<SearchResult[]> {
|
|
73
|
+
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
|
|
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
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw lastError ?? new Error("Search failed after retries");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseBraveResults(html: string, max: number): SearchResult[] {
|
|
123
|
+
const $ = cheerioLoad(html);
|
|
124
|
+
const results: SearchResult[] = [];
|
|
125
|
+
|
|
126
|
+
$('div.snippet[data-type="web"]').each((_i, el) => {
|
|
127
|
+
if (results.length >= max) return false;
|
|
128
|
+
|
|
129
|
+
const $el = $(el);
|
|
130
|
+
const anchor = $el.find(".result-content a").first();
|
|
131
|
+
const href = anchor.attr("href") ?? "";
|
|
132
|
+
if (!href.startsWith("http")) return;
|
|
133
|
+
|
|
134
|
+
const title = $el.find(".title").first().text().trim();
|
|
135
|
+
const snippet = $el.find(".generic-snippet .content").first().text().trim();
|
|
136
|
+
|
|
137
|
+
if (title) {
|
|
138
|
+
results.push({ title, url: href, snippet });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// web_fetch — fetch a URL and extract readable text via cheerio
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
const DEFAULT_MAX_LENGTH = 16_000;
|
|
150
|
+
|
|
151
|
+
function extractReadableText($: CheerioAPI, maxLength: number): { title: string; content: string } {
|
|
152
|
+
const title = $("title").first().text().trim();
|
|
153
|
+
|
|
154
|
+
$("script, style, noscript, nav, footer, header, aside, [role='navigation'], [role='banner'], [role='contentinfo']").remove();
|
|
155
|
+
$("svg, iframe, form, button, input, select, textarea").remove();
|
|
156
|
+
|
|
157
|
+
let root = $("article").first();
|
|
158
|
+
if (!root.length) root = $("main").first();
|
|
159
|
+
if (!root.length) root = $("[role='main']").first();
|
|
160
|
+
if (!root.length) root = $("body").first();
|
|
161
|
+
|
|
162
|
+
const text = root
|
|
163
|
+
.text()
|
|
164
|
+
.replace(/[ \t]+/g, " ")
|
|
165
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
166
|
+
.trim();
|
|
167
|
+
|
|
168
|
+
const content =
|
|
169
|
+
text.length > maxLength ? text.slice(0, maxLength) + "\n…(truncated)" : text;
|
|
170
|
+
|
|
171
|
+
return { title, content };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Tool definitions
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
export const createSearchTools = (): ToolDefinition[] => [
|
|
179
|
+
defineTool({
|
|
180
|
+
name: "web_search",
|
|
181
|
+
description:
|
|
182
|
+
"Search the web and return a list of results (title, URL, snippet). " +
|
|
183
|
+
"Use this instead of opening a browser when you need to find information online.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
query: {
|
|
188
|
+
type: "string",
|
|
189
|
+
description: "The search query",
|
|
190
|
+
},
|
|
191
|
+
max_results: {
|
|
192
|
+
type: "number",
|
|
193
|
+
description: "Maximum number of results to return (1-10, default 5)",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ["query"],
|
|
197
|
+
additionalProperties: false,
|
|
198
|
+
},
|
|
199
|
+
handler: async (input) => {
|
|
200
|
+
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
201
|
+
if (!query) {
|
|
202
|
+
return { error: "A non-empty query string is required." };
|
|
203
|
+
}
|
|
204
|
+
const max = Math.min(Math.max(Number(input.max_results) || 5, 1), 10);
|
|
205
|
+
try {
|
|
206
|
+
const results = await braveSearch(query, max);
|
|
207
|
+
if (results.length === 0) {
|
|
208
|
+
return { query, results: [], note: "No results found. Try rephrasing your query." };
|
|
209
|
+
}
|
|
210
|
+
return { query, results };
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
213
|
+
return {
|
|
214
|
+
error: `Search failed: ${msg}`,
|
|
215
|
+
hint: "The search provider may be rate-limiting requests. Try again shortly, or use browser tools as a fallback.",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
|
|
221
|
+
defineTool({
|
|
222
|
+
name: "web_fetch",
|
|
223
|
+
description:
|
|
224
|
+
"Fetch a web page and return its text content (HTML tags stripped). " +
|
|
225
|
+
"Useful for reading articles, documentation, or any web page without opening a browser.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
url: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "The URL to fetch",
|
|
232
|
+
},
|
|
233
|
+
max_length: {
|
|
234
|
+
type: "number",
|
|
235
|
+
description: `Maximum character length of returned content (default ${DEFAULT_MAX_LENGTH})`,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
required: ["url"],
|
|
239
|
+
additionalProperties: false,
|
|
240
|
+
},
|
|
241
|
+
handler: async (input) => {
|
|
242
|
+
const url = typeof input.url === "string" ? input.url.trim() : "";
|
|
243
|
+
if (!url) {
|
|
244
|
+
return { error: 'A "url" string is required.' };
|
|
245
|
+
}
|
|
246
|
+
const maxLength = Math.max(Number(input.max_length) || DEFAULT_MAX_LENGTH, 1_000);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const res = await fetch(url, {
|
|
250
|
+
headers: { "User-Agent": SEARCH_UA, Accept: "text/html,application/xhtml+xml" },
|
|
251
|
+
redirect: "follow",
|
|
252
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
253
|
+
});
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
return { url, status: res.status, error: res.statusText };
|
|
256
|
+
}
|
|
257
|
+
const html = await res.text();
|
|
258
|
+
const $ = cheerioLoad(html);
|
|
259
|
+
const { title, content } = extractReadableText($, maxLength);
|
|
260
|
+
return { url, status: res.status, title, content };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
263
|
+
return { url, error: `Fetch failed: ${msg}` };
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
];
|
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 {
|