@poncho-ai/harness 0.34.1 → 0.36.0
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 +12 -11
- package/.turbo/turbo-lint.log +6 -0
- package/.turbo/turbo-test.log +27100 -0
- package/CHANGELOG.md +37 -0
- package/dist/chunk-MCKGQKYU.js +15 -0
- package/dist/dist-3KMQR4IO.js +27092 -0
- package/dist/index.d.ts +553 -29
- package/dist/index.js +3132 -1902
- package/dist/isolate-5MISBSUK.js +733 -0
- package/dist/isolate-5R6762YA.js +605 -0
- package/dist/isolate-KUZ5NOPG.js +727 -0
- package/dist/isolate-LOL3T7RA.js +729 -0
- package/dist/isolate-N22X4TCE.js +740 -0
- package/dist/isolate-T7WXM7IL.js +1490 -0
- package/dist/isolate-TCWTUVG4.js +1532 -0
- package/dist/isolate-WFOLANOB.js +768 -0
- package/package.json +24 -4
- package/scripts/migrate-to-engine.mjs +556 -0
- package/src/config.ts +112 -1
- package/src/harness.ts +282 -91
- package/src/index.ts +7 -0
- package/src/isolate/bindings.ts +206 -0
- package/src/isolate/bundler.ts +179 -0
- package/src/isolate/index.ts +10 -0
- package/src/isolate/polyfills.ts +796 -0
- package/src/isolate/run-code-tool.ts +220 -0
- package/src/isolate/runtime.ts +286 -0
- package/src/isolate/type-stubs.ts +196 -0
- package/src/mcp.ts +140 -9
- package/src/memory.ts +142 -191
- package/src/reminder-store.ts +7 -235
- package/src/reminder-tools.ts +15 -2
- package/src/secrets-store.ts +163 -0
- package/src/state.ts +22 -1291
- package/src/storage/engine.ts +106 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/memory-engine.ts +588 -0
- package/src/storage/postgres-engine.ts +139 -0
- package/src/storage/schema.ts +145 -0
- package/src/storage/sql-dialect.ts +963 -0
- package/src/storage/sqlite-engine.ts +99 -0
- package/src/storage/store-adapters.ts +100 -0
- package/src/subagent-manager.ts +1 -0
- package/src/subagent-tools.ts +1 -0
- package/src/telemetry.ts +5 -1
- package/src/tenant-token.ts +42 -0
- package/src/todo-tools.ts +1 -136
- package/src/upload-store.ts +1 -0
- package/src/vfs/bash-manager.ts +120 -0
- package/src/vfs/bash-tool.ts +59 -0
- package/src/vfs/create-bash-fs.ts +32 -0
- package/src/vfs/edit-file-tool.ts +72 -0
- package/src/vfs/index.ts +5 -0
- package/src/vfs/poncho-fs-adapter.ts +267 -0
- package/src/vfs/protected-fs.ts +177 -0
- package/src/vfs/read-file-tool.ts +103 -0
- package/src/vfs/write-file-tool.ts +49 -0
- package/test/harness.test.ts +30 -36
- package/test/isolate-vfs.test.ts +453 -0
- package/test/isolate.test.ts +252 -0
- package/test/state.test.ts +4 -27
- package/test/storage-engine.test.ts +250 -0
- package/test/vfs.test.ts +242 -0
- package/src/kv-store.ts +0 -216
package/src/harness.ts
CHANGED
|
@@ -14,17 +14,33 @@ import type {
|
|
|
14
14
|
} from "@poncho-ai/sdk";
|
|
15
15
|
import { defineTool, getTextContent } from "@poncho-ai/sdk";
|
|
16
16
|
import type { UploadStore } from "./upload-store.js";
|
|
17
|
-
import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
|
|
17
|
+
import { PONCHO_UPLOAD_SCHEME, VFS_SCHEME, deriveUploadKey } from "./upload-store.js";
|
|
18
|
+
import type { StorageEngine } from "./storage/engine.js";
|
|
19
|
+
import { createStorageEngine, type StorageProvider } from "./storage/index.js";
|
|
20
|
+
import {
|
|
21
|
+
createConversationStoreFromEngine,
|
|
22
|
+
createMemoryStoreFromEngine,
|
|
23
|
+
createTodoStoreFromEngine,
|
|
24
|
+
createReminderStoreFromEngine,
|
|
25
|
+
} from "./storage/store-adapters.js";
|
|
26
|
+
import { BashEnvironmentManager } from "./vfs/bash-manager.js";
|
|
27
|
+
import { createBashTool } from "./vfs/bash-tool.js";
|
|
28
|
+
import { createReadFileTool } from "./vfs/read-file-tool.js";
|
|
29
|
+
import { createEditFileTool } from "./vfs/edit-file-tool.js";
|
|
30
|
+
import { createWriteFileTool } from "./vfs/write-file-tool.js";
|
|
31
|
+
import { PonchoFsAdapter } from "./vfs/poncho-fs-adapter.js";
|
|
18
32
|
import { parseAgentFile, parseAgentMarkdown, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
|
|
19
33
|
import { loadPonchoConfig, resolveMemoryConfig, resolveStateConfig, type PonchoConfig, type ToolAccess, type BuiltInToolToggles } from "./config.js";
|
|
20
|
-
import {
|
|
34
|
+
import { ponchoDocsTool } from "./default-tools.js";
|
|
21
35
|
import {
|
|
22
36
|
createMemoryStore,
|
|
23
37
|
createMemoryTools,
|
|
38
|
+
type MemoryConfig,
|
|
24
39
|
type MemoryStore,
|
|
25
40
|
} from "./memory.js";
|
|
26
41
|
import { createTodoStore, createTodoTools, type TodoItem, type TodoStore } from "./todo-tools.js";
|
|
27
42
|
import { createReminderStore, type ReminderStore } from "./reminder-store.js";
|
|
43
|
+
import { createSecretsStore, resolveEnv, type SecretsStore } from "./secrets-store.js";
|
|
28
44
|
import { createReminderTools } from "./reminder-tools.js";
|
|
29
45
|
import { LocalMcpBridge } from "./mcp.js";
|
|
30
46
|
import { createModelProvider, getModelContextWindow, type ModelProviderFactory, type ProviderConfig } from "./model-factory.js";
|
|
@@ -610,14 +626,14 @@ function extractMediaFromToolOutput(output: unknown): {
|
|
|
610
626
|
obj.type === "file" &&
|
|
611
627
|
typeof obj.data === "string" &&
|
|
612
628
|
typeof obj.mediaType === "string" &&
|
|
613
|
-
(obj.mediaType as string).startsWith("image/")
|
|
629
|
+
((obj.mediaType as string).startsWith("image/") || obj.mediaType === "application/pdf")
|
|
614
630
|
) {
|
|
615
631
|
mediaItems.push({
|
|
616
632
|
type: "media",
|
|
617
633
|
data: obj.data as string,
|
|
618
634
|
mediaType: obj.mediaType as string,
|
|
619
635
|
});
|
|
620
|
-
return { type: "file", mediaType: obj.mediaType, filename: obj.filename ?? "
|
|
636
|
+
return { type: "file", mediaType: obj.mediaType, filename: obj.filename ?? "file", _stripped: true };
|
|
621
637
|
}
|
|
622
638
|
const out: Record<string, unknown> = {};
|
|
623
639
|
for (const [k, v] of Object.entries(obj)) out[k] = walk(v);
|
|
@@ -639,8 +655,11 @@ export class AgentHarness {
|
|
|
639
655
|
readonly uploadStore?: UploadStore;
|
|
640
656
|
private skillContextWindow = "";
|
|
641
657
|
private memoryStore?: MemoryStore;
|
|
658
|
+
private readonly tenantMemoryStores = new Map<string, MemoryStore>();
|
|
659
|
+
private memoryConfig?: MemoryConfig;
|
|
642
660
|
private todoStore?: TodoStore;
|
|
643
661
|
reminderStore?: ReminderStore;
|
|
662
|
+
secretsStore?: SecretsStore;
|
|
644
663
|
private loadedConfig?: PonchoConfig;
|
|
645
664
|
private loadedSkills: SkillMetadata[] = [];
|
|
646
665
|
private skillFingerprint = "";
|
|
@@ -662,6 +681,11 @@ export class AgentHarness {
|
|
|
662
681
|
private subagentManager?: SubagentManager;
|
|
663
682
|
private readonly archivedToolResultsByConversation = new Map<string, Record<string, ArchivedToolResult>>();
|
|
664
683
|
|
|
684
|
+
/** Unified storage engine (replaces individual KV-backed stores). */
|
|
685
|
+
storageEngine?: StorageEngine;
|
|
686
|
+
/** Bash environment manager (creates per-tenant bash instances). */
|
|
687
|
+
private bashManager?: BashEnvironmentManager;
|
|
688
|
+
|
|
665
689
|
private resolveToolAccess(toolName: string): ToolAccess {
|
|
666
690
|
const tools = this.loadedConfig?.tools;
|
|
667
691
|
if (!tools) return true;
|
|
@@ -718,23 +742,8 @@ export class AgentHarness {
|
|
|
718
742
|
}
|
|
719
743
|
|
|
720
744
|
private registerConfiguredBuiltInTools(config: PonchoConfig | undefined): void {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
this.registerIfMissing(tool);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
if (this.isToolEnabled("write_file")) {
|
|
727
|
-
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
728
|
-
}
|
|
729
|
-
if (this.isToolEnabled("edit_file")) {
|
|
730
|
-
this.registerIfMissing(createEditTool(this.workingDir));
|
|
731
|
-
}
|
|
732
|
-
if (this.isToolEnabled("delete_file")) {
|
|
733
|
-
this.registerIfMissing(createDeleteTool(this.workingDir));
|
|
734
|
-
}
|
|
735
|
-
if (this.isToolEnabled("delete_directory")) {
|
|
736
|
-
this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
|
|
737
|
-
}
|
|
745
|
+
// Old file tools (read_file, write_file, etc.) are replaced by the bash tool.
|
|
746
|
+
// Only register search tools, poncho_docs, and get_tool_result_by_id.
|
|
738
747
|
for (const tool of createSearchTools()) {
|
|
739
748
|
if (this.isToolEnabled(tool.name)) {
|
|
740
749
|
this.registerIfMissing(tool);
|
|
@@ -799,6 +808,33 @@ export class AgentHarness {
|
|
|
799
808
|
});
|
|
800
809
|
}
|
|
801
810
|
|
|
811
|
+
private createVfsAccess(tenantId: string): NonNullable<ToolContext["vfs"]> {
|
|
812
|
+
const adapter = this.bashManager!.getAdapter(tenantId);
|
|
813
|
+
return {
|
|
814
|
+
readFile: (path: string) => adapter.readFileBuffer(path),
|
|
815
|
+
readText: (path: string) => adapter.readFile(path),
|
|
816
|
+
writeFile: (path: string, content: Uint8Array, mimeType?: string) =>
|
|
817
|
+
adapter.writeFile(path, content),
|
|
818
|
+
writeText: (path: string, content: string) =>
|
|
819
|
+
adapter.writeFile(path, content),
|
|
820
|
+
exists: (path: string) => adapter.exists(path),
|
|
821
|
+
stat: async (path: string) => {
|
|
822
|
+
const s = await adapter.stat(path);
|
|
823
|
+
return {
|
|
824
|
+
size: s.size,
|
|
825
|
+
isDirectory: s.isDirectory,
|
|
826
|
+
mimeType: undefined,
|
|
827
|
+
updatedAt: s.mtime.getTime(),
|
|
828
|
+
};
|
|
829
|
+
},
|
|
830
|
+
readdir: (path: string) => adapter.readdir(path),
|
|
831
|
+
mkdir: (path: string, options?: { recursive?: boolean }) =>
|
|
832
|
+
adapter.mkdir(path, options),
|
|
833
|
+
rm: (path: string, options?: { recursive?: boolean }) =>
|
|
834
|
+
adapter.rm(path, options),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
802
838
|
private shouldEnableWriteTool(): boolean {
|
|
803
839
|
const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
|
|
804
840
|
if (override === "1" || override === "true" || override === "yes") {
|
|
@@ -975,6 +1011,35 @@ export class AgentHarness {
|
|
|
975
1011
|
return this.todoStore.get(conversationId);
|
|
976
1012
|
}
|
|
977
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* Get a memory store, optionally scoped to a tenant.
|
|
1016
|
+
* Returns the default (agent-wide) store when tenantId is null/undefined.
|
|
1017
|
+
*/
|
|
1018
|
+
private getMemoryStore(tenantId?: string): MemoryStore | undefined {
|
|
1019
|
+
if (!this.memoryConfig?.enabled) return undefined;
|
|
1020
|
+
if (!tenantId) return this.memoryStore;
|
|
1021
|
+
|
|
1022
|
+
let store = this.tenantMemoryStores.get(tenantId);
|
|
1023
|
+
if (!store) {
|
|
1024
|
+
if (this.storageEngine) {
|
|
1025
|
+
store = createMemoryStoreFromEngine(this.storageEngine, tenantId);
|
|
1026
|
+
} else {
|
|
1027
|
+
const agentId = this.parsedAgent?.frontmatter.id ?? this.parsedAgent?.frontmatter.name ?? "unknown";
|
|
1028
|
+
store = createMemoryStore(agentId, this.memoryConfig, {
|
|
1029
|
+
workingDir: this.workingDir,
|
|
1030
|
+
tenantId,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
this.tenantMemoryStores.set(tenantId, store);
|
|
1034
|
+
// Evict oldest entries if cache grows too large
|
|
1035
|
+
if (this.tenantMemoryStores.size > 100) {
|
|
1036
|
+
const oldest = this.tenantMemoryStores.keys().next().value;
|
|
1037
|
+
if (oldest) this.tenantMemoryStores.delete(oldest);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return store;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
978
1043
|
private listActiveSkills(): string[] {
|
|
979
1044
|
return [...this.activeSkillNames].sort();
|
|
980
1045
|
}
|
|
@@ -1164,6 +1229,7 @@ export class AgentHarness {
|
|
|
1164
1229
|
if (!this.mcpBridge) {
|
|
1165
1230
|
return;
|
|
1166
1231
|
}
|
|
1232
|
+
|
|
1167
1233
|
const requestedPatterns = this.getRequestedMcpPatterns();
|
|
1168
1234
|
this.dispatcher.unregisterMany(this.registeredMcpToolNames);
|
|
1169
1235
|
this.registeredMcpToolNames.clear();
|
|
@@ -1354,29 +1420,74 @@ export class AgentHarness {
|
|
|
1354
1420
|
this.registerSkillTools(skillMetadata);
|
|
1355
1421
|
const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
|
|
1356
1422
|
|
|
1423
|
+
// --- Unified Storage Engine ---
|
|
1424
|
+
const storageProvider = (config?.storage?.provider ?? "sqlite") as StorageProvider;
|
|
1425
|
+
const engine = createStorageEngine({
|
|
1426
|
+
provider: storageProvider,
|
|
1427
|
+
workingDir: this.workingDir,
|
|
1428
|
+
agentId,
|
|
1429
|
+
urlEnv: config?.storage?.urlEnv,
|
|
1430
|
+
});
|
|
1431
|
+
await engine.initialize();
|
|
1432
|
+
this.storageEngine = engine;
|
|
1433
|
+
|
|
1434
|
+
// --- Bash Environment Manager ---
|
|
1435
|
+
const maxFileSize = config?.storage?.limits?.maxFileSize ?? 100 * 1024 * 1024; // 100MB
|
|
1436
|
+
const maxTotalStorage = config?.storage?.limits?.maxTotalStorage ?? 1024 * 1024 * 1024; // 1GB
|
|
1437
|
+
const bashWorkingDir = this.environment === "production" ? null : this.workingDir;
|
|
1438
|
+
this.bashManager = new BashEnvironmentManager(
|
|
1439
|
+
engine,
|
|
1440
|
+
{ maxFileSize, maxTotalStorage },
|
|
1441
|
+
bashWorkingDir,
|
|
1442
|
+
config?.bash,
|
|
1443
|
+
config?.network,
|
|
1444
|
+
);
|
|
1445
|
+
// Register VFS tools
|
|
1446
|
+
this.registerIfMissing(createBashTool(this.bashManager));
|
|
1447
|
+
this.registerIfMissing(createReadFileTool(engine));
|
|
1448
|
+
this.registerIfMissing(createEditFileTool(engine));
|
|
1449
|
+
this.registerIfMissing(createWriteFileTool(engine));
|
|
1450
|
+
|
|
1451
|
+
// --- Isolate (V8 sandboxed code execution) ---
|
|
1452
|
+
if (config?.isolate) {
|
|
1453
|
+
const { createRunCodeTool, buildRunCodeDescription, bundleLibraries } = await import("./isolate/index.js");
|
|
1454
|
+
let libraryPreamble: string | null = null;
|
|
1455
|
+
if (config.isolate.libraries?.length) {
|
|
1456
|
+
libraryPreamble = await bundleLibraries(config.isolate.libraries, this.workingDir);
|
|
1457
|
+
}
|
|
1458
|
+
const runCodeTool = createRunCodeTool({
|
|
1459
|
+
config: config.isolate,
|
|
1460
|
+
bashManager: this.bashManager,
|
|
1461
|
+
libraryPreamble,
|
|
1462
|
+
description: buildRunCodeDescription(config.isolate, !!config.network),
|
|
1463
|
+
network: config.network,
|
|
1464
|
+
});
|
|
1465
|
+
this.registerIfMissing(runCodeTool);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// --- Memory (engine-backed or legacy fallback) ---
|
|
1469
|
+
this.memoryConfig = memoryConfig ?? undefined;
|
|
1357
1470
|
if (memoryConfig?.enabled) {
|
|
1358
|
-
this.memoryStore =
|
|
1359
|
-
agentId,
|
|
1360
|
-
memoryConfig,
|
|
1361
|
-
{ workingDir: this.workingDir },
|
|
1362
|
-
);
|
|
1471
|
+
this.memoryStore = createMemoryStoreFromEngine(engine);
|
|
1363
1472
|
this.dispatcher.registerMany(
|
|
1364
|
-
createMemoryTools(
|
|
1365
|
-
|
|
1366
|
-
|
|
1473
|
+
createMemoryTools(
|
|
1474
|
+
(ctx) => this.getMemoryStore(ctx.tenantId) ?? this.memoryStore!,
|
|
1475
|
+
{ maxRecallConversations: memoryConfig.maxRecallConversations },
|
|
1476
|
+
),
|
|
1367
1477
|
);
|
|
1368
1478
|
}
|
|
1369
1479
|
|
|
1370
|
-
|
|
1371
|
-
this.todoStore =
|
|
1480
|
+
// --- Todos (engine-backed) ---
|
|
1481
|
+
this.todoStore = createTodoStoreFromEngine(engine);
|
|
1372
1482
|
for (const tool of createTodoTools(this.todoStore)) {
|
|
1373
1483
|
if (this.isToolEnabled(tool.name)) {
|
|
1374
1484
|
this.registerIfMissing(tool);
|
|
1375
1485
|
}
|
|
1376
1486
|
}
|
|
1377
1487
|
|
|
1488
|
+
// --- Reminders (engine-backed) ---
|
|
1378
1489
|
if (config?.reminders?.enabled) {
|
|
1379
|
-
this.reminderStore =
|
|
1490
|
+
this.reminderStore = createReminderStoreFromEngine(engine);
|
|
1380
1491
|
for (const tool of createReminderTools(this.reminderStore)) {
|
|
1381
1492
|
if (this.isToolEnabled(tool.name)) {
|
|
1382
1493
|
this.registerIfMissing(tool);
|
|
@@ -1393,6 +1504,17 @@ export class AgentHarness {
|
|
|
1393
1504
|
});
|
|
1394
1505
|
}
|
|
1395
1506
|
|
|
1507
|
+
// Secrets store for per-tenant env var overrides
|
|
1508
|
+
const stateConfig = resolveStateConfig(config);
|
|
1509
|
+
const authTokenEnv = config?.auth?.tokenEnv ?? "PONCHO_AUTH_TOKEN";
|
|
1510
|
+
const authToken = process.env[authTokenEnv];
|
|
1511
|
+
if (authToken) {
|
|
1512
|
+
this.secretsStore = createSecretsStore(agentId, authToken, stateConfig, { workingDir: this.workingDir });
|
|
1513
|
+
bridge.setEnvResolver(async (tenantId, envName) => {
|
|
1514
|
+
return resolveEnv(this.secretsStore, tenantId, envName);
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1396
1518
|
await bridge.startLocalServers();
|
|
1397
1519
|
await bridge.discoverTools();
|
|
1398
1520
|
await this.refreshMcpTools("initialize");
|
|
@@ -1444,60 +1566,22 @@ export class AgentHarness {
|
|
|
1444
1566
|
};
|
|
1445
1567
|
}
|
|
1446
1568
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
const
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
const
|
|
1454
|
-
return {
|
|
1455
|
-
async save(json: string) {
|
|
1456
|
-
await fetch(`${baseUrl}/set/${encodeURIComponent(stateKey)}/${encodeURIComponent(json)}`, { method: "POST", headers });
|
|
1457
|
-
},
|
|
1458
|
-
async load() {
|
|
1459
|
-
const res = await fetch(`${baseUrl}/get/${encodeURIComponent(stateKey)}`, { headers });
|
|
1460
|
-
if (!res.ok) return undefined;
|
|
1461
|
-
const body = await res.json() as { result?: string | null };
|
|
1462
|
-
return body.result ?? undefined;
|
|
1463
|
-
},
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
if (provider === "redis") {
|
|
1468
|
-
const urlEnv = config.storage?.urlEnv ?? "REDIS_URL";
|
|
1469
|
-
const url = process.env[urlEnv] ?? "";
|
|
1470
|
-
if (!url) return undefined;
|
|
1471
|
-
let clientPromise: Promise<{ get(k: string): Promise<string | null>; set(k: string, v: string): Promise<unknown> } | undefined> | undefined;
|
|
1472
|
-
const getClient = () => {
|
|
1473
|
-
if (!clientPromise) {
|
|
1474
|
-
clientPromise = (async () => {
|
|
1475
|
-
try {
|
|
1476
|
-
const mod = (await import("redis")) as unknown as {
|
|
1477
|
-
createClient: (opts: { url: string }) => {
|
|
1478
|
-
connect(): Promise<unknown>;
|
|
1479
|
-
get(k: string): Promise<string | null>;
|
|
1480
|
-
set(k: string, v: string): Promise<unknown>;
|
|
1481
|
-
};
|
|
1482
|
-
};
|
|
1483
|
-
const c = mod.createClient({ url });
|
|
1484
|
-
await c.connect();
|
|
1485
|
-
return c;
|
|
1486
|
-
} catch { return undefined; }
|
|
1487
|
-
})();
|
|
1488
|
-
}
|
|
1489
|
-
return clientPromise;
|
|
1490
|
-
};
|
|
1569
|
+
// For sqlite, postgresql, and all other providers: use local file persistence
|
|
1570
|
+
// (same as "local" above). The old upstash/redis branches have been removed.
|
|
1571
|
+
if (provider === "sqlite" || provider === "postgresql") {
|
|
1572
|
+
const { resolve: pathResolve } = await import("node:path");
|
|
1573
|
+
const { homedir: home } = await import("node:os");
|
|
1574
|
+
const stateDir = pathResolve(home(), ".poncho", "browser-state");
|
|
1575
|
+
const filePath = pathResolve(stateDir, `${sessionId}.json`);
|
|
1491
1576
|
return {
|
|
1492
1577
|
async save(json: string) {
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1578
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
1579
|
+
await mkdir(stateDir, { recursive: true });
|
|
1580
|
+
await writeFile(filePath, json, "utf8");
|
|
1495
1581
|
},
|
|
1496
1582
|
async load() {
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
const val = await c.get(stateKey);
|
|
1500
|
-
return val ?? undefined;
|
|
1583
|
+
const { readFile } = await import("node:fs/promises");
|
|
1584
|
+
try { return await readFile(filePath, "utf8"); } catch { return undefined; }
|
|
1501
1585
|
},
|
|
1502
1586
|
};
|
|
1503
1587
|
}
|
|
@@ -1589,12 +1673,20 @@ export class AgentHarness {
|
|
|
1589
1673
|
this.otlpTracerProvider = undefined;
|
|
1590
1674
|
}
|
|
1591
1675
|
this.hasOtlpExporter = false;
|
|
1676
|
+
|
|
1677
|
+
// Cleanup bash environments and storage engine
|
|
1678
|
+
this.bashManager?.destroyAll();
|
|
1679
|
+
await this.storageEngine?.close();
|
|
1592
1680
|
}
|
|
1593
1681
|
|
|
1594
1682
|
listTools(): ToolDefinition[] {
|
|
1595
1683
|
return this.dispatcher.list();
|
|
1596
1684
|
}
|
|
1597
1685
|
|
|
1686
|
+
listSkills(): Array<{ name: string; description: string }> {
|
|
1687
|
+
return this.loadedSkills.map((s) => ({ name: s.name, description: s.description }));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1598
1690
|
/**
|
|
1599
1691
|
* Wraps the run() generator with an OTel root span (invoke_agent) so all
|
|
1600
1692
|
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
@@ -1609,6 +1701,7 @@ export class AgentHarness {
|
|
|
1609
1701
|
attributes: {
|
|
1610
1702
|
"gen_ai.operation.name": "invoke_agent",
|
|
1611
1703
|
...(input.conversationId ? { "gen_ai.conversation.id": input.conversationId } : {}),
|
|
1704
|
+
...(input.tenantId ? { "tenant.id": input.tenantId } : {}),
|
|
1612
1705
|
},
|
|
1613
1706
|
});
|
|
1614
1707
|
|
|
@@ -1662,8 +1755,9 @@ export class AgentHarness {
|
|
|
1662
1755
|
await this.initialize();
|
|
1663
1756
|
}
|
|
1664
1757
|
// Start memory + todo fetches early so they overlap with refresh I/O
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1758
|
+
const activeMemoryStore = this.getMemoryStore(input.tenantId);
|
|
1759
|
+
const memoryPromise = activeMemoryStore
|
|
1760
|
+
? activeMemoryStore.getMainMemory()
|
|
1667
1761
|
: undefined;
|
|
1668
1762
|
const todosPromise = this.todoStore
|
|
1669
1763
|
? this.todoStore.get(input.conversationId ?? "__default__")
|
|
@@ -1671,6 +1765,16 @@ export class AgentHarness {
|
|
|
1671
1765
|
await this.refreshAgentIfChanged();
|
|
1672
1766
|
await this.refreshSkillsIfChanged();
|
|
1673
1767
|
|
|
1768
|
+
// Deferred MCP discovery: servers that couldn't discover at startup because the
|
|
1769
|
+
// env var was missing (tenant secrets provide the token instead).
|
|
1770
|
+
if (input.tenantId && this.mcpBridge?.hasDeferredServers()) {
|
|
1771
|
+
const newTools = await this.mcpBridge.discoverAndLoadDeferred(input.tenantId);
|
|
1772
|
+
for (const tool of newTools) {
|
|
1773
|
+
this.dispatcher.register(tool);
|
|
1774
|
+
this.registeredMcpToolNames.add(tool.name);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1674
1778
|
let agent = this.parsedAgent as ParsedAgent;
|
|
1675
1779
|
const runId = `run_${randomUUID()}`;
|
|
1676
1780
|
const start = now();
|
|
@@ -1763,11 +1867,56 @@ ${boundedMainMemory.trim()}`
|
|
|
1763
1867
|
? `\n\n## Open Tasks\n\n${openTodos.map((t) => `- [${t.status === "in_progress" ? "IN PROGRESS" : "PENDING"}] ${t.content} (id: ${t.id})`).join("\n")}`
|
|
1764
1868
|
: "";
|
|
1765
1869
|
|
|
1870
|
+
const fsContext = this.bashManager
|
|
1871
|
+
? `\n\n## Filesystem
|
|
1872
|
+
|
|
1873
|
+
You have a persistent virtual filesystem at \`/\`. Files you create are durable across conversations.
|
|
1874
|
+
Use the \`bash\` tool for all file operations (cat, echo, grep, awk, jq, sed, find, etc.).
|
|
1875
|
+
|
|
1876
|
+
Filesystem layout:
|
|
1877
|
+
- \`/\` — your working directory (persistent, database-backed)${
|
|
1878
|
+
this.environment !== "production"
|
|
1879
|
+
? `\n- \`/project/\` — the project source code (read-write in dev; protected paths like .env, .git/ are blocked)`
|
|
1880
|
+
: ""
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
Examples:${
|
|
1884
|
+
this.environment !== "production"
|
|
1885
|
+
? `\n- Read a project file: \`cat /project/src/index.ts\``
|
|
1886
|
+
: ""
|
|
1887
|
+
}
|
|
1888
|
+
- Write a working file: \`echo "data" > /notes.txt\`
|
|
1889
|
+
- Process data: \`cat /data.csv | awk -F, '{print $2}' | sort | uniq -c\`
|
|
1890
|
+
|
|
1891
|
+
Files in the VFS are accessible to the user via \`/api/vfs/{path}\`. For example, a file at \`/downloads/report.pdf\` can be linked as \`/api/vfs/downloads/report.pdf\`. Use this to share downloadable files with the user.`
|
|
1892
|
+
: "";
|
|
1893
|
+
|
|
1894
|
+
// Isolate context (code execution guidance + type stubs)
|
|
1895
|
+
let isolateContext = "";
|
|
1896
|
+
if (this.loadedConfig?.isolate && this.dispatcher.get("run_code")) {
|
|
1897
|
+
const { generateIsolateTypeStubs } = await import("./isolate/index.js");
|
|
1898
|
+
const typeStubs = generateIsolateTypeStubs(this.loadedConfig.isolate);
|
|
1899
|
+
isolateContext = `\n\n## Code Execution
|
|
1900
|
+
|
|
1901
|
+
You have a \`run_code\` tool for executing JavaScript/TypeScript in a sandboxed V8 isolate.
|
|
1902
|
+
|
|
1903
|
+
**When to use \`run_code\` vs \`bash\`:**
|
|
1904
|
+
- \`bash\`: file manipulation, text processing with unix tools, shell pipelines
|
|
1905
|
+
- \`run_code\`: complex data processing, structured data, npm libraries, multi-step logic, binary file generation
|
|
1906
|
+
|
|
1907
|
+
**API reference (available inside the isolate):**
|
|
1908
|
+
\`\`\`typescript
|
|
1909
|
+
${typeStubs}
|
|
1910
|
+
\`\`\`
|
|
1911
|
+
|
|
1912
|
+
Code is wrapped in an async IIFE — use \`return\` to return a value to the tool result.`;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1766
1915
|
const buildSystemPrompt = (): string => {
|
|
1767
1916
|
const agentPrompt = renderCurrentAgentPrompt();
|
|
1768
1917
|
const promptWithSkills = this.skillContextWindow
|
|
1769
|
-
? `${agentPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
|
|
1770
|
-
: `${agentPrompt}${developmentContext}${browserContext}`;
|
|
1918
|
+
? `${agentPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}${fsContext}${isolateContext}`
|
|
1919
|
+
: `${agentPrompt}${developmentContext}${browserContext}${fsContext}${isolateContext}`;
|
|
1771
1920
|
const timeContext = this.reminderStore
|
|
1772
1921
|
? `\n\nCurrent UTC time: ${new Date().toISOString()}`
|
|
1773
1922
|
: "";
|
|
@@ -1970,9 +2119,37 @@ ${boundedMainMemory.trim()}`
|
|
|
1970
2119
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1971
2120
|
const rich = (meta as any)?._richToolResults as unknown[] | undefined;
|
|
1972
2121
|
if (rich && rich.length > 0) {
|
|
1973
|
-
//
|
|
1974
|
-
//
|
|
1975
|
-
//
|
|
2122
|
+
// Resolve any vfs:// references in media items before sending
|
|
2123
|
+
// to the model. This keeps conversation history lightweight
|
|
2124
|
+
// (only stores the reference) while materializing the actual
|
|
2125
|
+
// bytes on demand at model-request time.
|
|
2126
|
+
if (this.storageEngine) {
|
|
2127
|
+
const tid = input.tenantId ?? "__default__";
|
|
2128
|
+
for (const part of rich) {
|
|
2129
|
+
const p = part as Record<string, unknown>;
|
|
2130
|
+
if (p.output && typeof p.output === "object") {
|
|
2131
|
+
const out = p.output as Record<string, unknown>;
|
|
2132
|
+
if (Array.isArray(out.value)) {
|
|
2133
|
+
for (let i = 0; i < out.value.length; i++) {
|
|
2134
|
+
const item = out.value[i] as Record<string, unknown>;
|
|
2135
|
+
if (
|
|
2136
|
+
item.type === "media" &&
|
|
2137
|
+
typeof item.data === "string" &&
|
|
2138
|
+
(item.data as string).startsWith(VFS_SCHEME)
|
|
2139
|
+
) {
|
|
2140
|
+
try {
|
|
2141
|
+
const vfsPath = (item.data as string).slice(VFS_SCHEME.length);
|
|
2142
|
+
const buf = await this.storageEngine.vfs.readFile(tid, vfsPath);
|
|
2143
|
+
item.data = Buffer.from(buf).toString("base64");
|
|
2144
|
+
} catch {
|
|
2145
|
+
// File no longer available; leave as-is
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
1976
2153
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1977
2154
|
return [{ role: "tool" as const, content: rich as any }];
|
|
1978
2155
|
}
|
|
@@ -2126,7 +2303,11 @@ ${boundedMainMemory.trim()}`
|
|
|
2126
2303
|
if (!isSupportedImage && !isSupportedFile && isTextBasedMime(part.mediaType)) {
|
|
2127
2304
|
let textContent: string;
|
|
2128
2305
|
try {
|
|
2129
|
-
if (part.data.startsWith(
|
|
2306
|
+
if (part.data.startsWith(VFS_SCHEME) && this.storageEngine) {
|
|
2307
|
+
const vfsPath = part.data.slice(VFS_SCHEME.length);
|
|
2308
|
+
const buf = await this.storageEngine.vfs.readFile(input.tenantId ?? "__default__", vfsPath);
|
|
2309
|
+
textContent = Buffer.from(buf).toString("utf8");
|
|
2310
|
+
} else if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
|
|
2130
2311
|
const buf = await this.uploadStore.get(part.data);
|
|
2131
2312
|
textContent = buf.toString("utf8");
|
|
2132
2313
|
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
@@ -2155,7 +2336,11 @@ ${boundedMainMemory.trim()}`
|
|
|
2155
2336
|
// fetch URLs itself (which fails for private blob stores).
|
|
2156
2337
|
let resolvedData: string;
|
|
2157
2338
|
try {
|
|
2158
|
-
if (part.data.startsWith(
|
|
2339
|
+
if (part.data.startsWith(VFS_SCHEME) && this.storageEngine) {
|
|
2340
|
+
const vfsPath = part.data.slice(VFS_SCHEME.length);
|
|
2341
|
+
const buf = await this.storageEngine.vfs.readFile(input.tenantId ?? "__default__", vfsPath);
|
|
2342
|
+
resolvedData = Buffer.from(buf).toString("base64");
|
|
2343
|
+
} else if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
|
|
2159
2344
|
const buf = await this.uploadStore.get(part.data);
|
|
2160
2345
|
resolvedData = buf.toString("base64");
|
|
2161
2346
|
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
@@ -2593,6 +2778,10 @@ ${boundedMainMemory.trim()}`
|
|
|
2593
2778
|
parameters: input.parameters ?? {},
|
|
2594
2779
|
abortSignal: input.abortSignal,
|
|
2595
2780
|
conversationId: input.conversationId,
|
|
2781
|
+
tenantId: input.tenantId,
|
|
2782
|
+
vfs: this.bashManager
|
|
2783
|
+
? this.createVfsAccess(input.tenantId ?? "__default__")
|
|
2784
|
+
: undefined,
|
|
2596
2785
|
};
|
|
2597
2786
|
|
|
2598
2787
|
const toolResultsForModel: Array<{
|
|
@@ -2785,6 +2974,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2785
2974
|
return;
|
|
2786
2975
|
}
|
|
2787
2976
|
|
|
2977
|
+
const callInputMap = new Map(approvedCalls.map((c) => [c.id, c.input]));
|
|
2788
2978
|
for (const result of batchResults) {
|
|
2789
2979
|
const span = toolSpans.get(result.callId);
|
|
2790
2980
|
if (result.error) {
|
|
@@ -2838,6 +3028,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2838
3028
|
yield pushEvent({
|
|
2839
3029
|
type: "tool:completed",
|
|
2840
3030
|
tool: result.tool,
|
|
3031
|
+
input: callInputMap.get(result.callId),
|
|
2841
3032
|
output: result.output,
|
|
2842
3033
|
duration: now() - batchStart,
|
|
2843
3034
|
outputTokenEstimate,
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,13 @@ export * from "./reminder-tools.js";
|
|
|
17
17
|
export * from "./state.js";
|
|
18
18
|
export * from "./upload-store.js";
|
|
19
19
|
export * from "./telemetry.js";
|
|
20
|
+
export * from "./secrets-store.js";
|
|
21
|
+
export * from "./storage/index.js";
|
|
22
|
+
export * from "./storage/store-adapters.js";
|
|
23
|
+
export { PonchoFsAdapter } from "./vfs/poncho-fs-adapter.js";
|
|
24
|
+
export { BashEnvironmentManager } from "./vfs/bash-manager.js";
|
|
25
|
+
export { createBashTool } from "./vfs/bash-tool.js";
|
|
26
|
+
export * from "./tenant-token.js";
|
|
20
27
|
export * from "./tool-dispatcher.js";
|
|
21
28
|
export * from "./subagent-manager.js";
|
|
22
29
|
export * from "./subagent-tools.js";
|