@poncho-ai/harness 0.15.1 → 0.16.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.15.1 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.16.1 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 182.55 KB
11
- ESM ⚡️ Build success in 131ms
10
+ ESM dist/index.js 187.36 KB
11
+ ESM ⚡️ Build success in 91ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 6605ms
14
- DTS dist/index.d.ts 22.34 KB
13
+ DTS ⚡️ Build success in 3621ms
14
+ DTS dist/index.d.ts 22.38 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`7475da5`](https://github.com/cesr/poncho-ai/commit/7475da5c0c2399e79064a2622137c0eb2fb16871) Thanks [@cesr](https://github.com/cesr)! - Inject browser usage context into agent system prompt (auth flow, session persistence, tool selection guidance).
8
+
9
+ ## 0.16.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`12f2845`](https://github.com/cesr/poncho-ai/commit/12f28457c20e650640ff2a1c1dbece2a6e4a9ddd) Thanks [@cesr](https://github.com/cesr)! - Add browser storage persistence (cookies/localStorage survive restarts via configured storage provider) and new `browser_content` tool for fast text extraction from pages.
14
+
3
15
  ## 0.15.1
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -486,6 +486,7 @@ declare class AgentHarness {
486
486
  private registerSkillTools;
487
487
  private refreshSkillsIfChanged;
488
488
  initialize(): Promise<void>;
489
+ private buildBrowserStoragePersistence;
489
490
  private initBrowserTools;
490
491
  /** Conversation ID of the currently executing run (set during run, cleared after). */
491
492
  private _currentRunConversationId?;
package/dist/index.js CHANGED
@@ -3290,6 +3290,85 @@ var AgentHarness = class {
3290
3290
  );
3291
3291
  }
3292
3292
  }
3293
+ async buildBrowserStoragePersistence(config, sessionId) {
3294
+ const provider = config.storage?.provider ?? config.state?.provider ?? "local";
3295
+ const stateKey = `poncho:browser:state:${sessionId}`;
3296
+ if (provider === "memory") return void 0;
3297
+ if (provider === "local") {
3298
+ const { resolve: pathResolve } = await import("path");
3299
+ const { homedir: home } = await import("os");
3300
+ const stateDir = pathResolve(home(), ".poncho", "browser-state");
3301
+ const filePath = pathResolve(stateDir, `${sessionId}.json`);
3302
+ return {
3303
+ async save(json) {
3304
+ const { mkdir: mkdir5, writeFile: writeFile6 } = await import("fs/promises");
3305
+ await mkdir5(stateDir, { recursive: true });
3306
+ await writeFile6(filePath, json, "utf8");
3307
+ },
3308
+ async load() {
3309
+ const { readFile: readFile8 } = await import("fs/promises");
3310
+ try {
3311
+ return await readFile8(filePath, "utf8");
3312
+ } catch {
3313
+ return void 0;
3314
+ }
3315
+ }
3316
+ };
3317
+ }
3318
+ if (provider === "upstash") {
3319
+ const urlEnv = config.storage?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
3320
+ const tokenEnv = config.storage?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
3321
+ const baseUrl = (process.env[urlEnv] ?? "").replace(/\/+$/, "");
3322
+ const token = process.env[tokenEnv] ?? "";
3323
+ if (!baseUrl || !token) return void 0;
3324
+ const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
3325
+ return {
3326
+ async save(json) {
3327
+ await fetch(`${baseUrl}/set/${encodeURIComponent(stateKey)}/${encodeURIComponent(json)}`, { method: "POST", headers });
3328
+ },
3329
+ async load() {
3330
+ const res = await fetch(`${baseUrl}/get/${encodeURIComponent(stateKey)}`, { headers });
3331
+ if (!res.ok) return void 0;
3332
+ const body = await res.json();
3333
+ return body.result ?? void 0;
3334
+ }
3335
+ };
3336
+ }
3337
+ if (provider === "redis") {
3338
+ const urlEnv = config.storage?.urlEnv ?? "REDIS_URL";
3339
+ const url = process.env[urlEnv] ?? "";
3340
+ if (!url) return void 0;
3341
+ let clientPromise;
3342
+ const getClient = () => {
3343
+ if (!clientPromise) {
3344
+ clientPromise = (async () => {
3345
+ try {
3346
+ const mod = await import("redis");
3347
+ const c = mod.createClient({ url });
3348
+ await c.connect();
3349
+ return c;
3350
+ } catch {
3351
+ return void 0;
3352
+ }
3353
+ })();
3354
+ }
3355
+ return clientPromise;
3356
+ };
3357
+ return {
3358
+ async save(json) {
3359
+ const c = await getClient();
3360
+ if (c) await c.set(stateKey, json);
3361
+ },
3362
+ async load() {
3363
+ const c = await getClient();
3364
+ if (!c) return void 0;
3365
+ const val = await c.get(stateKey);
3366
+ return val ?? void 0;
3367
+ }
3368
+ };
3369
+ }
3370
+ return void 0;
3371
+ }
3293
3372
  async initBrowserTools(config) {
3294
3373
  const spec = ["@poncho-ai", "browser"].join("/");
3295
3374
  let browserMod;
@@ -3318,9 +3397,14 @@ var AgentHarness = class {
3318
3397
  );
3319
3398
  }
3320
3399
  this._browserMod = browserMod;
3321
- const browserCfg = typeof config.browser === "object" ? config.browser : {};
3400
+ const browserCfg = typeof config.browser === "object" ? { ...config.browser } : {};
3322
3401
  const agentId = this.parsedAgent?.frontmatter.id ?? this.parsedAgent?.frontmatter.name ?? "default";
3323
- const session = new browserMod.BrowserSession(`poncho-${agentId}`, browserCfg);
3402
+ const sessionId = `poncho-${agentId}`;
3403
+ const storagePersistence = await this.buildBrowserStoragePersistence(config, sessionId);
3404
+ if (storagePersistence) {
3405
+ browserCfg.storagePersistence = storagePersistence;
3406
+ }
3407
+ const session = new browserMod.BrowserSession(sessionId, browserCfg);
3324
3408
  this._browserSession = session;
3325
3409
  const tools = browserMod.createBrowserTools(
3326
3410
  () => session,
@@ -3454,9 +3538,29 @@ var AgentHarness = class {
3454
3538
  const developmentContext = this.environment === "development" ? `
3455
3539
 
3456
3540
  ${DEVELOPMENT_MODE_CONTEXT}` : "";
3541
+ const browserContext = this._browserSession ? `
3542
+
3543
+ ## Browser Tools
3544
+
3545
+ The user has a live browser viewport displayed alongside the conversation. They can see everything the browser shows in real time and interact with it directly (click, type, scroll, paste).
3546
+
3547
+ ### Authentication
3548
+ When a website requires authentication or credentials, do NOT ask the user to send them in the chat. Instead, navigate to the login page and let the user enter their credentials directly in the browser viewport. Wait for them to confirm they have logged in before continuing.
3549
+
3550
+ ### Session persistence
3551
+ Browser sessions (cookies, localStorage, login state) are automatically saved and restored across conversations. If the user logged into a website in a previous conversation, that session is likely still active. Try navigating directly to the authenticated page before asking the user to log in again.
3552
+
3553
+ ### Reading page content
3554
+ - Use \`browser_content\` to read the visible text on a page. This is fast and token-efficient.
3555
+ - Use \`browser_snapshot\` to get the accessibility tree with interactive element refs for clicking and typing.
3556
+ - Use \`browser_screenshot\` only when you need to see visual layout or images. Screenshots consume significantly more tokens.
3557
+ - The accessibility tree may be sparse on some pages. If \`browser_snapshot\` returns little or no content, fall back to \`browser_content\` or \`browser_screenshot\`.
3558
+
3559
+ ### Tabs and resources
3560
+ Each conversation gets its own browser tab sharing a single browser instance. Call \`browser_close\` when done to free the tab. If you don't close it, the tab stays open and the user can continue interacting with it.` : "";
3457
3561
  const promptWithSkills = this.skillContextWindow ? `${systemPrompt}${developmentContext}
3458
3562
 
3459
- ${this.skillContextWindow}` : `${systemPrompt}${developmentContext}`;
3563
+ ${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentContext}${browserContext}`;
3460
3564
  const mainMemory = this.memoryStore ? await this.memoryStore.getMainMemory() : void 0;
3461
3565
  const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
3462
3566
  ...[truncated]` : mainMemory?.content;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.15.1",
3
+ "version": "0.16.1",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
package/src/harness.ts CHANGED
@@ -914,6 +914,94 @@ export class AgentHarness {
914
914
  }
915
915
  }
916
916
 
917
+ private async buildBrowserStoragePersistence(
918
+ config: PonchoConfig,
919
+ sessionId: string,
920
+ ): Promise<{ save(json: string): Promise<void>; load(): Promise<string | undefined> } | undefined> {
921
+ const provider = config.storage?.provider ?? (config.state as Record<string, unknown> | undefined)?.provider as string | undefined ?? "local";
922
+ const stateKey = `poncho:browser:state:${sessionId}`;
923
+
924
+ if (provider === "memory") return undefined;
925
+
926
+ if (provider === "local") {
927
+ const { resolve: pathResolve } = await import("node:path");
928
+ const { homedir: home } = await import("node:os");
929
+ const stateDir = pathResolve(home(), ".poncho", "browser-state");
930
+ const filePath = pathResolve(stateDir, `${sessionId}.json`);
931
+ return {
932
+ async save(json: string) {
933
+ const { mkdir, writeFile } = await import("node:fs/promises");
934
+ await mkdir(stateDir, { recursive: true });
935
+ await writeFile(filePath, json, "utf8");
936
+ },
937
+ async load() {
938
+ const { readFile } = await import("node:fs/promises");
939
+ try { return await readFile(filePath, "utf8"); } catch { return undefined; }
940
+ },
941
+ };
942
+ }
943
+
944
+ if (provider === "upstash") {
945
+ const urlEnv = config.storage?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
946
+ const tokenEnv = config.storage?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
947
+ const baseUrl = (process.env[urlEnv] ?? "").replace(/\/+$/, "");
948
+ const token = process.env[tokenEnv] ?? "";
949
+ if (!baseUrl || !token) return undefined;
950
+ const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
951
+ return {
952
+ async save(json: string) {
953
+ await fetch(`${baseUrl}/set/${encodeURIComponent(stateKey)}/${encodeURIComponent(json)}`, { method: "POST", headers });
954
+ },
955
+ async load() {
956
+ const res = await fetch(`${baseUrl}/get/${encodeURIComponent(stateKey)}`, { headers });
957
+ if (!res.ok) return undefined;
958
+ const body = await res.json() as { result?: string | null };
959
+ return body.result ?? undefined;
960
+ },
961
+ };
962
+ }
963
+
964
+ if (provider === "redis") {
965
+ const urlEnv = config.storage?.urlEnv ?? "REDIS_URL";
966
+ const url = process.env[urlEnv] ?? "";
967
+ if (!url) return undefined;
968
+ let clientPromise: Promise<{ get(k: string): Promise<string | null>; set(k: string, v: string): Promise<unknown> } | undefined> | undefined;
969
+ const getClient = () => {
970
+ if (!clientPromise) {
971
+ clientPromise = (async () => {
972
+ try {
973
+ const mod = (await import("redis")) as unknown as {
974
+ createClient: (opts: { url: string }) => {
975
+ connect(): Promise<unknown>;
976
+ get(k: string): Promise<string | null>;
977
+ set(k: string, v: string): Promise<unknown>;
978
+ };
979
+ };
980
+ const c = mod.createClient({ url });
981
+ await c.connect();
982
+ return c;
983
+ } catch { return undefined; }
984
+ })();
985
+ }
986
+ return clientPromise;
987
+ };
988
+ return {
989
+ async save(json: string) {
990
+ const c = await getClient();
991
+ if (c) await c.set(stateKey, json);
992
+ },
993
+ async load() {
994
+ const c = await getClient();
995
+ if (!c) return undefined;
996
+ const val = await c.get(stateKey);
997
+ return val ?? undefined;
998
+ },
999
+ };
1000
+ }
1001
+
1002
+ return undefined;
1003
+ }
1004
+
917
1005
  private async initBrowserTools(config: PonchoConfig): Promise<void> {
918
1006
  const spec = ["@poncho-ai", "browser"].join("/");
919
1007
  let browserMod: {
@@ -947,9 +1035,16 @@ export class AgentHarness {
947
1035
  }
948
1036
 
949
1037
  this._browserMod = browserMod;
950
- const browserCfg = typeof config.browser === "object" ? config.browser : {};
1038
+ const browserCfg: Record<string, unknown> = typeof config.browser === "object" ? { ...config.browser } : {};
951
1039
  const agentId = this.parsedAgent?.frontmatter.id ?? this.parsedAgent?.frontmatter.name ?? "default";
952
- const session = new browserMod.BrowserSession(`poncho-${agentId}`, browserCfg);
1040
+ const sessionId = `poncho-${agentId}`;
1041
+
1042
+ const storagePersistence = await this.buildBrowserStoragePersistence(config, sessionId);
1043
+ if (storagePersistence) {
1044
+ browserCfg.storagePersistence = storagePersistence;
1045
+ }
1046
+
1047
+ const session = new browserMod.BrowserSession(sessionId, browserCfg);
953
1048
  this._browserSession = session;
954
1049
 
955
1050
  const tools = browserMod.createBrowserTools(
@@ -1115,9 +1210,29 @@ export class AgentHarness {
1115
1210
  });
1116
1211
  const developmentContext =
1117
1212
  this.environment === "development" ? `\n\n${DEVELOPMENT_MODE_CONTEXT}` : "";
1213
+ const browserContext = this._browserSession
1214
+ ? `\n\n## Browser Tools
1215
+
1216
+ The user has a live browser viewport displayed alongside the conversation. They can see everything the browser shows in real time and interact with it directly (click, type, scroll, paste).
1217
+
1218
+ ### Authentication
1219
+ When a website requires authentication or credentials, do NOT ask the user to send them in the chat. Instead, navigate to the login page and let the user enter their credentials directly in the browser viewport. Wait for them to confirm they have logged in before continuing.
1220
+
1221
+ ### Session persistence
1222
+ Browser sessions (cookies, localStorage, login state) are automatically saved and restored across conversations. If the user logged into a website in a previous conversation, that session is likely still active. Try navigating directly to the authenticated page before asking the user to log in again.
1223
+
1224
+ ### Reading page content
1225
+ - Use \`browser_content\` to read the visible text on a page. This is fast and token-efficient.
1226
+ - Use \`browser_snapshot\` to get the accessibility tree with interactive element refs for clicking and typing.
1227
+ - Use \`browser_screenshot\` only when you need to see visual layout or images. Screenshots consume significantly more tokens.
1228
+ - The accessibility tree may be sparse on some pages. If \`browser_snapshot\` returns little or no content, fall back to \`browser_content\` or \`browser_screenshot\`.
1229
+
1230
+ ### Tabs and resources
1231
+ Each conversation gets its own browser tab sharing a single browser instance. Call \`browser_close\` when done to free the tab. If you don't close it, the tab stays open and the user can continue interacting with it.`
1232
+ : "";
1118
1233
  const promptWithSkills = this.skillContextWindow
1119
- ? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}`
1120
- : `${systemPrompt}${developmentContext}`;
1234
+ ? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
1235
+ : `${systemPrompt}${developmentContext}${browserContext}`;
1121
1236
  const mainMemory = this.memoryStore
1122
1237
  ? await this.memoryStore.getMainMemory()
1123
1238
  : undefined;