@poncho-ai/harness 0.40.1 → 0.41.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 +5 -5
- package/CHANGELOG.md +86 -0
- package/dist/index.d.ts +44 -3
- package/dist/index.js +131 -36
- package/package.json +1 -1
- package/src/harness.ts +81 -18
- package/src/mcp.ts +102 -23
- package/src/storage/engine.ts +2 -0
- package/src/storage/memory-engine.ts +1 -1
- package/src/storage/sql-dialect.ts +1 -1
- package/test/harness-config-injection.test.ts +63 -0
- package/test/harness-injection.test.ts +93 -0
- package/test/mcp-tenant-cache.test.ts +311 -0
- package/test/mcp.test.ts +174 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.41.0 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,9 +8,9 @@
|
|
|
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[
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m493.42 KB[39m
|
|
12
12
|
[32mESM[39m [1mdist/isolate-VY35DGLM.js [22m[32m49.43 KB[39m
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 238ms
|
|
14
14
|
[34mDTS[39m Build start
|
|
15
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
15
|
+
[32mDTS[39m ⚡️ Build success in 7385ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m77.00 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.41.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#110](https://github.com/cesr/poncho-ai/pull/110) [`7d57a88`](https://github.com/cesr/poncho-ai/commit/7d57a88e55a49ec04de3dbd415b2440bb727e31f) Thanks [@cesr](https://github.com/cesr)! - harness: allow programmatic agent + storage injection (no AGENT.md required)
|
|
8
|
+
|
|
9
|
+
`HarnessOptions` gains two optional fields that let callers construct a
|
|
10
|
+
`Harness` without an `AGENT.md` on disk and without the
|
|
11
|
+
`ensureAgentIdentity` filesystem dance:
|
|
12
|
+
- `agentDefinition?: string | ParsedAgent` — raw markdown or a pre-parsed
|
|
13
|
+
agent. When provided, `initialize()` skips the `AGENT.md` read.
|
|
14
|
+
- `storageEngine?: StorageEngine` — pre-constructed engine; required
|
|
15
|
+
whenever `agentDefinition` is provided. The engine's `agentId` (now a
|
|
16
|
+
public readonly field on the `StorageEngine` interface) becomes the
|
|
17
|
+
source of truth for partitioning, and is mirrored onto
|
|
18
|
+
`parsedAgent.frontmatter.id` so existing downstream readers continue
|
|
19
|
+
to resolve correctly.
|
|
20
|
+
|
|
21
|
+
When neither field is provided, behaviour is unchanged: the harness
|
|
22
|
+
reads `AGENT.md` from `workingDir`, calls `ensureAgentIdentity`, and
|
|
23
|
+
constructs the `StorageEngine` internally.
|
|
24
|
+
|
|
25
|
+
`refreshAgentIfChanged()` short-circuits when an agent definition was
|
|
26
|
+
injected — callers who update an agent re-instantiate the harness
|
|
27
|
+
rather than relying on disk file watching.
|
|
28
|
+
|
|
29
|
+
This is the first of a small set of changes that lets `@poncho-ai/harness`
|
|
30
|
+
be embedded as a library by consumer SaaS apps where each user has
|
|
31
|
+
their own per-tenant agent state in a database, no filesystem layout.
|
|
32
|
+
|
|
33
|
+
- [#111](https://github.com/cesr/poncho-ai/pull/111) [`ac18616`](https://github.com/cesr/poncho-ai/commit/ac18616b864189c91d0957c72c537933497505f4) Thanks [@cesr](https://github.com/cesr)! - harness: allow programmatic `PonchoConfig` injection
|
|
34
|
+
|
|
35
|
+
`HarnessOptions` gains an optional `config?: PonchoConfig` field. When
|
|
36
|
+
provided, `initialize()` skips `loadPonchoConfig` (which imports
|
|
37
|
+
`poncho.config.js` from `workingDir`) and uses the supplied object
|
|
38
|
+
directly. Downstream resolvers (`resolveMemoryConfig`,
|
|
39
|
+
`resolveStateConfig`, etc.) run as today, so any validation/normalization
|
|
40
|
+
they perform applies to injected configs identically.
|
|
41
|
+
|
|
42
|
+
Behaviour is unchanged when the field is absent: the disk loader runs
|
|
43
|
+
as before.
|
|
44
|
+
|
|
45
|
+
This is part of a small series of changes that enables
|
|
46
|
+
`@poncho-ai/harness` to be embedded as a library by a consumer SaaS
|
|
47
|
+
where each user's agent configuration comes from a database row, not a
|
|
48
|
+
`poncho.config.js` on disk.
|
|
49
|
+
|
|
50
|
+
- [#112](https://github.com/cesr/poncho-ai/pull/112) [`c22416b`](https://github.com/cesr/poncho-ai/commit/c22416b3d4c4557277aeabf53e70877be6436e85) Thanks [@cesr](https://github.com/cesr)! - harness: cache MCP clients per `(serverName, tenantId)` instead of rebuilding per call
|
|
51
|
+
|
|
52
|
+
When a tenant resolves a different bearer token than the host's
|
|
53
|
+
`process.env` default for an MCP server, the per-call handler used to
|
|
54
|
+
construct a brand-new `StreamableHttpMcpRpcClient` on every tool call.
|
|
55
|
+
For builders this rarely triggered. For consumer/SaaS deployments where
|
|
56
|
+
**every** call resolves a different per-user token, every tool call
|
|
57
|
+
forced a fresh `initialize` round-trip — no session reuse, high
|
|
58
|
+
latency, and a behaviour the recently-added 404 session-retry can't
|
|
59
|
+
help with because there was nothing to retry.
|
|
60
|
+
|
|
61
|
+
`LocalMcpBridge` now keeps a `Map<key, { client, token, lastUsed }>`
|
|
62
|
+
keyed by `(serverName, tenantId)`. Lookups reuse the cached client when
|
|
63
|
+
the token is unchanged and the entry is within the configured idle TTL
|
|
64
|
+
(default 15 minutes). On token rotation or TTL expiry the entry is
|
|
65
|
+
evicted lazily and rebuilt. `stopLocalServers()` closes all cached
|
|
66
|
+
tenant clients alongside the server-default ones.
|
|
67
|
+
|
|
68
|
+
The TTL is configurable via a constructor option (`tenantClientTtlMs`)
|
|
69
|
+
for tests and tuning.
|
|
70
|
+
|
|
71
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- [#109](https://github.com/cesr/poncho-ai/pull/109) [`4b5d974`](https://github.com/cesr/poncho-ai/commit/4b5d974345733ac9e68f36201dff7e7d8a8f0327) Thanks [@cesr](https://github.com/cesr)! - harness: re-initialize MCP session on 404 instead of staying wedged
|
|
74
|
+
|
|
75
|
+
Streamable-HTTP MCP clients with session state (e.g. Arcade's gateway
|
|
76
|
+
for Gmail / Google Calendar) issue an `Mcp-Session-Id` on initialize
|
|
77
|
+
and expire it after some idle window. The bridge cached `sessionId`
|
|
78
|
+
and `initialized` in process memory and never reset them, so once the
|
|
79
|
+
server returned 404 for a stale session every subsequent tool call
|
|
80
|
+
also 404'd until the host process restarted. Long-lived deployments
|
|
81
|
+
(e.g. Railway) hit this; serverless platforms masked it because each
|
|
82
|
+
invocation re-initialized.
|
|
83
|
+
|
|
84
|
+
The client now treats `404` with a stored `sessionId` as a session
|
|
85
|
+
expiry signal: it clears the session, re-runs `initialize`, and
|
|
86
|
+
retries the request once. A 404 from initialize itself (no session
|
|
87
|
+
yet) is still treated as a hard endpoint failure with no retry.
|
|
88
|
+
|
|
3
89
|
## 0.40.1
|
|
4
90
|
|
|
5
91
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -370,12 +370,26 @@ declare class LocalMcpBridge {
|
|
|
370
370
|
private readonly unavailableServers;
|
|
371
371
|
private readonly authFailedServers;
|
|
372
372
|
private envResolver?;
|
|
373
|
+
/**
|
|
374
|
+
* Per-tenant MCP client cache. For consumer/SaaS deployments where every
|
|
375
|
+
* call resolves a different bearer token, building a fresh
|
|
376
|
+
* `StreamableHttpMcpRpcClient` per call would force a fresh `initialize`
|
|
377
|
+
* round-trip every time. We keep one client per `(serverName, tenantId)`
|
|
378
|
+
* with TTL-based idle eviction; on token rotation we evict the entry
|
|
379
|
+
* lazily and rebuild.
|
|
380
|
+
*/
|
|
381
|
+
private readonly tenantClients;
|
|
382
|
+
/** Test/observability hook: bumped every time a new tenant client is constructed. */
|
|
383
|
+
tenantClientConstructions: number;
|
|
384
|
+
private readonly tenantClientTtlMs;
|
|
373
385
|
/**
|
|
374
386
|
* Set a resolver for per-tenant env vars (e.g. MCP auth tokens).
|
|
375
387
|
* Called by the harness after creating the secrets store.
|
|
376
388
|
*/
|
|
377
389
|
setEnvResolver(resolver: (tenantId: string | undefined, envName: string) => Promise<string | undefined>): void;
|
|
378
|
-
constructor(config: McpConfig | undefined
|
|
390
|
+
constructor(config: McpConfig | undefined, options?: {
|
|
391
|
+
tenantClientTtlMs?: number;
|
|
392
|
+
});
|
|
379
393
|
private getServerName;
|
|
380
394
|
private log;
|
|
381
395
|
/** Set of servers where discovery was deferred (no default token, has env resolver). */
|
|
@@ -389,6 +403,7 @@ declare class LocalMcpBridge {
|
|
|
389
403
|
private tryDeferredDiscovery;
|
|
390
404
|
startLocalServers(): Promise<void>;
|
|
391
405
|
stopLocalServers(): Promise<void>;
|
|
406
|
+
private getOrCreateTenantClient;
|
|
392
407
|
listServers(): RemoteMcpServerConfig[];
|
|
393
408
|
listRemoteServers(): RemoteMcpServerConfig[];
|
|
394
409
|
checkRemoteConnectivity(): Promise<Array<{
|
|
@@ -806,6 +821,8 @@ interface VfsDirEntry {
|
|
|
806
821
|
type: "file" | "directory" | "symlink";
|
|
807
822
|
}
|
|
808
823
|
interface StorageEngine {
|
|
824
|
+
/** Partition key: every read/write is scoped to this agent id. */
|
|
825
|
+
readonly agentId: string;
|
|
809
826
|
/** Run migrations and prepare the storage backend. */
|
|
810
827
|
initialize(): Promise<void>;
|
|
811
828
|
/** Gracefully release resources. */
|
|
@@ -1007,6 +1024,27 @@ interface HarnessOptions {
|
|
|
1007
1024
|
toolDefinitions?: ToolDefinition[];
|
|
1008
1025
|
modelProvider?: ModelProviderFactory;
|
|
1009
1026
|
uploadStore?: UploadStore;
|
|
1027
|
+
/**
|
|
1028
|
+
* Inject the agent definition directly instead of reading AGENT.md from
|
|
1029
|
+
* `workingDir`. Pass raw markdown (string) or a pre-parsed `ParsedAgent`.
|
|
1030
|
+
* When provided, `storageEngine` is also required — the engine's
|
|
1031
|
+
* `agentId` becomes the source of truth for partitioning, and the
|
|
1032
|
+
* filesystem identity dance (`ensureAgentIdentity`) is skipped.
|
|
1033
|
+
*/
|
|
1034
|
+
agentDefinition?: string | ParsedAgent;
|
|
1035
|
+
/**
|
|
1036
|
+
* Pre-constructed storage engine. When provided, the harness will not
|
|
1037
|
+
* create one internally. The engine's `agentId` is used wherever the
|
|
1038
|
+
* harness today reads `parsedAgent.frontmatter.id`.
|
|
1039
|
+
*/
|
|
1040
|
+
storageEngine?: StorageEngine;
|
|
1041
|
+
/**
|
|
1042
|
+
* Inject a `PonchoConfig` object directly instead of importing
|
|
1043
|
+
* `poncho.config.js` from `workingDir`. When provided, the disk-based
|
|
1044
|
+
* loader is skipped. Downstream resolvers (`resolveMemoryConfig`,
|
|
1045
|
+
* `resolveStateConfig`, etc.) run as today regardless of source.
|
|
1046
|
+
*/
|
|
1047
|
+
config?: PonchoConfig;
|
|
1010
1048
|
}
|
|
1011
1049
|
interface HarnessRunOutput {
|
|
1012
1050
|
runId: string;
|
|
@@ -1037,6 +1075,7 @@ declare class AgentHarness {
|
|
|
1037
1075
|
reminderStore?: ReminderStore;
|
|
1038
1076
|
secretsStore?: SecretsStore;
|
|
1039
1077
|
private loadedConfig?;
|
|
1078
|
+
private readonly injectedConfig?;
|
|
1040
1079
|
private loadedSkills;
|
|
1041
1080
|
private skillFingerprint;
|
|
1042
1081
|
private lastSkillRefreshAt;
|
|
@@ -1051,6 +1090,8 @@ declare class AgentHarness {
|
|
|
1051
1090
|
private _browserMod?;
|
|
1052
1091
|
private parsedAgent?;
|
|
1053
1092
|
private agentFileFingerprint;
|
|
1093
|
+
private injectedAgentDefinition?;
|
|
1094
|
+
private injectedStorageEngine;
|
|
1054
1095
|
private mcpBridge?;
|
|
1055
1096
|
private subagentManager?;
|
|
1056
1097
|
private readonly archivedToolResultsByConversation;
|
|
@@ -1279,7 +1320,7 @@ declare class TelemetryEmitter {
|
|
|
1279
1320
|
}
|
|
1280
1321
|
|
|
1281
1322
|
declare class InMemoryEngine implements StorageEngine {
|
|
1282
|
-
|
|
1323
|
+
readonly agentId: string;
|
|
1283
1324
|
private convs;
|
|
1284
1325
|
private mem;
|
|
1285
1326
|
private todoData;
|
|
@@ -1394,7 +1435,7 @@ declare class ConversationEgressMeter {
|
|
|
1394
1435
|
}
|
|
1395
1436
|
declare abstract class SqlStorageEngine implements StorageEngine {
|
|
1396
1437
|
protected readonly dialect: Dialect;
|
|
1397
|
-
|
|
1438
|
+
readonly agentId: string;
|
|
1398
1439
|
protected abstract readonly executor: QueryExecutor;
|
|
1399
1440
|
protected readonly egressMeter: ConversationEgressMeter;
|
|
1400
1441
|
constructor(dialect: Dialect, agentId: string);
|
package/dist/index.js
CHANGED
|
@@ -5911,6 +5911,11 @@ var McpHttpError = class extends Error {
|
|
|
5911
5911
|
this.status = status;
|
|
5912
5912
|
}
|
|
5913
5913
|
};
|
|
5914
|
+
var McpSessionExpiredError = class extends Error {
|
|
5915
|
+
constructor() {
|
|
5916
|
+
super("MCP session expired");
|
|
5917
|
+
}
|
|
5918
|
+
};
|
|
5914
5919
|
var StreamableHttpMcpRpcClient = class {
|
|
5915
5920
|
endpoint;
|
|
5916
5921
|
timeoutMs;
|
|
@@ -5962,6 +5967,9 @@ var StreamableHttpMcpRpcClient = class {
|
|
|
5962
5967
|
if (response.status === 403) {
|
|
5963
5968
|
throw new McpHttpError(403, "MCP server forbidden");
|
|
5964
5969
|
}
|
|
5970
|
+
if (response.status === 404 && this.sessionId) {
|
|
5971
|
+
throw new McpSessionExpiredError();
|
|
5972
|
+
}
|
|
5965
5973
|
if (!response.ok) {
|
|
5966
5974
|
throw new Error(`MCP HTTP request failed with status ${response.status}`);
|
|
5967
5975
|
}
|
|
@@ -6036,20 +6044,32 @@ var StreamableHttpMcpRpcClient = class {
|
|
|
6036
6044
|
throw new Error(`MCP response missing JSON-RPC payload for id ${id}`);
|
|
6037
6045
|
}
|
|
6038
6046
|
async request(method, params) {
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6047
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
6048
|
+
try {
|
|
6049
|
+
await this.ensureInitialized();
|
|
6050
|
+
const id = this.idCounter++;
|
|
6051
|
+
const payload = {
|
|
6052
|
+
jsonrpc: "2.0",
|
|
6053
|
+
id,
|
|
6054
|
+
method,
|
|
6055
|
+
params: params ?? {}
|
|
6056
|
+
};
|
|
6057
|
+
const payloads = await this.postMessage(payload);
|
|
6058
|
+
const result = this.extractResult(payloads, id);
|
|
6059
|
+
if (result.error) {
|
|
6060
|
+
throw new Error(result.error.message ?? `MCP error on ${method}`);
|
|
6061
|
+
}
|
|
6062
|
+
return result.result;
|
|
6063
|
+
} catch (error) {
|
|
6064
|
+
if (error instanceof McpSessionExpiredError && attempt === 0) {
|
|
6065
|
+
this.sessionId = void 0;
|
|
6066
|
+
this.initialized = false;
|
|
6067
|
+
continue;
|
|
6068
|
+
}
|
|
6069
|
+
throw error;
|
|
6070
|
+
}
|
|
6051
6071
|
}
|
|
6052
|
-
|
|
6072
|
+
throw new Error(`MCP request to ${method} failed after session retry`);
|
|
6053
6073
|
}
|
|
6054
6074
|
async ensureInitialized() {
|
|
6055
6075
|
if (this.initialized) {
|
|
@@ -6105,6 +6125,8 @@ var StreamableHttpMcpRpcClient = class {
|
|
|
6105
6125
|
}
|
|
6106
6126
|
}
|
|
6107
6127
|
};
|
|
6128
|
+
var TENANT_CLIENT_TTL_MS = 15 * 60 * 1e3;
|
|
6129
|
+
var tenantClientKey = (serverName, tenantId) => `${serverName}\0${tenantId}`;
|
|
6108
6130
|
var LocalMcpBridge = class {
|
|
6109
6131
|
remoteServers;
|
|
6110
6132
|
rpcClients = /* @__PURE__ */ new Map();
|
|
@@ -6112,6 +6134,18 @@ var LocalMcpBridge = class {
|
|
|
6112
6134
|
unavailableServers = /* @__PURE__ */ new Map();
|
|
6113
6135
|
authFailedServers = /* @__PURE__ */ new Set();
|
|
6114
6136
|
envResolver;
|
|
6137
|
+
/**
|
|
6138
|
+
* Per-tenant MCP client cache. For consumer/SaaS deployments where every
|
|
6139
|
+
* call resolves a different bearer token, building a fresh
|
|
6140
|
+
* `StreamableHttpMcpRpcClient` per call would force a fresh `initialize`
|
|
6141
|
+
* round-trip every time. We keep one client per `(serverName, tenantId)`
|
|
6142
|
+
* with TTL-based idle eviction; on token rotation we evict the entry
|
|
6143
|
+
* lazily and rebuild.
|
|
6144
|
+
*/
|
|
6145
|
+
tenantClients = /* @__PURE__ */ new Map();
|
|
6146
|
+
/** Test/observability hook: bumped every time a new tenant client is constructed. */
|
|
6147
|
+
tenantClientConstructions = 0;
|
|
6148
|
+
tenantClientTtlMs;
|
|
6115
6149
|
/**
|
|
6116
6150
|
* Set a resolver for per-tenant env vars (e.g. MCP auth tokens).
|
|
6117
6151
|
* Called by the harness after creating the secrets store.
|
|
@@ -6119,7 +6153,8 @@ var LocalMcpBridge = class {
|
|
|
6119
6153
|
setEnvResolver(resolver) {
|
|
6120
6154
|
this.envResolver = resolver;
|
|
6121
6155
|
}
|
|
6122
|
-
constructor(config) {
|
|
6156
|
+
constructor(config, options) {
|
|
6157
|
+
this.tenantClientTtlMs = options?.tenantClientTtlMs ?? TENANT_CLIENT_TTL_MS;
|
|
6123
6158
|
this.remoteServers = (config?.mcp ?? []).filter(
|
|
6124
6159
|
(entry) => typeof entry.url === "string"
|
|
6125
6160
|
);
|
|
@@ -6290,6 +6325,35 @@ var LocalMcpBridge = class {
|
|
|
6290
6325
|
await client.close();
|
|
6291
6326
|
}
|
|
6292
6327
|
this.rpcClients.clear();
|
|
6328
|
+
for (const [, entry] of this.tenantClients) {
|
|
6329
|
+
await entry.client.close();
|
|
6330
|
+
}
|
|
6331
|
+
this.tenantClients.clear();
|
|
6332
|
+
}
|
|
6333
|
+
getOrCreateTenantClient(serverName, tenantId, token, server) {
|
|
6334
|
+
const key = tenantClientKey(serverName, tenantId);
|
|
6335
|
+
const now2 = Date.now();
|
|
6336
|
+
const existing = this.tenantClients.get(key);
|
|
6337
|
+
if (existing) {
|
|
6338
|
+
const idle = now2 - existing.lastUsed > this.tenantClientTtlMs;
|
|
6339
|
+
const tokenChanged = existing.token !== token;
|
|
6340
|
+
if (idle || tokenChanged) {
|
|
6341
|
+
void existing.client.close();
|
|
6342
|
+
this.tenantClients.delete(key);
|
|
6343
|
+
} else {
|
|
6344
|
+
existing.lastUsed = now2;
|
|
6345
|
+
return existing.client;
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
const client = new StreamableHttpMcpRpcClient(
|
|
6349
|
+
server.url,
|
|
6350
|
+
server.timeoutMs ?? 1e4,
|
|
6351
|
+
token,
|
|
6352
|
+
server.headers
|
|
6353
|
+
);
|
|
6354
|
+
this.tenantClients.set(key, { client, token, lastUsed: now2 });
|
|
6355
|
+
this.tenantClientConstructions += 1;
|
|
6356
|
+
return client;
|
|
6293
6357
|
}
|
|
6294
6358
|
listServers() {
|
|
6295
6359
|
return [...this.remoteServers];
|
|
@@ -6411,15 +6475,15 @@ var LocalMcpBridge = class {
|
|
|
6411
6475
|
try {
|
|
6412
6476
|
const tokenEnv = server?.auth?.tokenEnv;
|
|
6413
6477
|
let callClient = client;
|
|
6414
|
-
if (tokenEnv && this.envResolver && context?.tenantId) {
|
|
6478
|
+
if (tokenEnv && this.envResolver && context?.tenantId && server) {
|
|
6415
6479
|
const tenantToken = await this.envResolver(context.tenantId, tokenEnv);
|
|
6416
6480
|
const defaultToken = process.env[tokenEnv];
|
|
6417
6481
|
if (tenantToken && tenantToken !== defaultToken) {
|
|
6418
|
-
callClient =
|
|
6419
|
-
|
|
6420
|
-
|
|
6482
|
+
callClient = this.getOrCreateTenantClient(
|
|
6483
|
+
serverName,
|
|
6484
|
+
context.tenantId,
|
|
6421
6485
|
tenantToken,
|
|
6422
|
-
server
|
|
6486
|
+
server
|
|
6423
6487
|
);
|
|
6424
6488
|
}
|
|
6425
6489
|
}
|
|
@@ -8640,6 +8704,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
8640
8704
|
reminderStore;
|
|
8641
8705
|
secretsStore;
|
|
8642
8706
|
loadedConfig;
|
|
8707
|
+
injectedConfig;
|
|
8643
8708
|
loadedSkills = [];
|
|
8644
8709
|
skillFingerprint = "";
|
|
8645
8710
|
lastSkillRefreshAt = 0;
|
|
@@ -8654,6 +8719,8 @@ var AgentHarness = class _AgentHarness {
|
|
|
8654
8719
|
_browserMod;
|
|
8655
8720
|
parsedAgent;
|
|
8656
8721
|
agentFileFingerprint = "";
|
|
8722
|
+
injectedAgentDefinition;
|
|
8723
|
+
injectedStorageEngine = false;
|
|
8657
8724
|
mcpBridge;
|
|
8658
8725
|
subagentManager;
|
|
8659
8726
|
archivedToolResultsByConversation = /* @__PURE__ */ new Map();
|
|
@@ -8821,6 +8888,17 @@ var AgentHarness = class _AgentHarness {
|
|
|
8821
8888
|
this.modelProviderInjected = !!options.modelProvider;
|
|
8822
8889
|
this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
|
|
8823
8890
|
this.uploadStore = options.uploadStore;
|
|
8891
|
+
this.injectedConfig = options.config;
|
|
8892
|
+
if (options.agentDefinition !== void 0 && options.storageEngine === void 0) {
|
|
8893
|
+
throw new Error(
|
|
8894
|
+
"HarnessOptions.agentDefinition requires HarnessOptions.storageEngine \u2014 construct a StorageEngine with the desired agentId and pass both."
|
|
8895
|
+
);
|
|
8896
|
+
}
|
|
8897
|
+
this.injectedAgentDefinition = options.agentDefinition;
|
|
8898
|
+
if (options.storageEngine) {
|
|
8899
|
+
this.storageEngine = options.storageEngine;
|
|
8900
|
+
this.injectedStorageEngine = true;
|
|
8901
|
+
}
|
|
8824
8902
|
if (options.toolDefinitions?.length) {
|
|
8825
8903
|
this.dispatcher.registerMany(options.toolDefinitions);
|
|
8826
8904
|
}
|
|
@@ -9237,6 +9315,9 @@ var AgentHarness = class _AgentHarness {
|
|
|
9237
9315
|
if (this.environment !== "development") {
|
|
9238
9316
|
return false;
|
|
9239
9317
|
}
|
|
9318
|
+
if (this.injectedAgentDefinition !== void 0) {
|
|
9319
|
+
return false;
|
|
9320
|
+
}
|
|
9240
9321
|
try {
|
|
9241
9322
|
const agentFilePath = resolve11(this.workingDir, "AGENT.md");
|
|
9242
9323
|
const rawContent = await readFile8(agentFilePath, "utf8");
|
|
@@ -9304,15 +9385,23 @@ var AgentHarness = class _AgentHarness {
|
|
|
9304
9385
|
}
|
|
9305
9386
|
}
|
|
9306
9387
|
async initialize() {
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9388
|
+
if (this.injectedAgentDefinition !== void 0) {
|
|
9389
|
+
this.parsedAgent = typeof this.injectedAgentDefinition === "string" ? parseAgentMarkdown(this.injectedAgentDefinition) : this.injectedAgentDefinition;
|
|
9390
|
+
this.agentFileFingerprint = "";
|
|
9391
|
+
if (this.storageEngine) {
|
|
9392
|
+
this.parsedAgent.frontmatter.id = this.storageEngine.agentId;
|
|
9393
|
+
}
|
|
9394
|
+
} else {
|
|
9395
|
+
const agentFilePath = resolve11(this.workingDir, "AGENT.md");
|
|
9396
|
+
const agentRawContent = await readFile8(agentFilePath, "utf8");
|
|
9397
|
+
this.parsedAgent = parseAgentMarkdown(agentRawContent);
|
|
9398
|
+
this.agentFileFingerprint = agentRawContent;
|
|
9399
|
+
const identity = await ensureAgentIdentity(this.workingDir);
|
|
9400
|
+
if (!this.parsedAgent.frontmatter.id) {
|
|
9401
|
+
this.parsedAgent.frontmatter.id = identity.id;
|
|
9402
|
+
}
|
|
9314
9403
|
}
|
|
9315
|
-
const config = await loadPonchoConfig(this.workingDir);
|
|
9404
|
+
const config = this.injectedConfig ?? await loadPonchoConfig(this.workingDir);
|
|
9316
9405
|
this.loadedConfig = config;
|
|
9317
9406
|
this.registerConfiguredBuiltInTools(config);
|
|
9318
9407
|
const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
|
|
@@ -9328,15 +9417,21 @@ var AgentHarness = class _AgentHarness {
|
|
|
9328
9417
|
this.skillFingerprint = this.buildSkillFingerprint(skillMetadata);
|
|
9329
9418
|
this.registerSkillTools();
|
|
9330
9419
|
const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9420
|
+
let engine;
|
|
9421
|
+
if (this.injectedStorageEngine && this.storageEngine) {
|
|
9422
|
+
engine = this.storageEngine;
|
|
9423
|
+
await engine.initialize();
|
|
9424
|
+
} else {
|
|
9425
|
+
const storageProvider = config?.storage?.provider ?? "sqlite";
|
|
9426
|
+
engine = createStorageEngine({
|
|
9427
|
+
provider: storageProvider,
|
|
9428
|
+
workingDir: this.workingDir,
|
|
9429
|
+
agentId,
|
|
9430
|
+
urlEnv: config?.storage?.urlEnv
|
|
9431
|
+
});
|
|
9432
|
+
await engine.initialize();
|
|
9433
|
+
this.storageEngine = engine;
|
|
9434
|
+
}
|
|
9340
9435
|
const maxFileSize = config?.storage?.limits?.maxFileSize ?? 100 * 1024 * 1024;
|
|
9341
9436
|
const maxTotalStorage = config?.storage?.limits?.maxTotalStorage ?? 1024 * 1024 * 1024;
|
|
9342
9437
|
const bashWorkingDir = this.environment === "production" ? null : this.workingDir;
|
package/package.json
CHANGED
package/src/harness.ts
CHANGED
|
@@ -102,6 +102,27 @@ export interface HarnessOptions {
|
|
|
102
102
|
toolDefinitions?: ToolDefinition[];
|
|
103
103
|
modelProvider?: ModelProviderFactory;
|
|
104
104
|
uploadStore?: UploadStore;
|
|
105
|
+
/**
|
|
106
|
+
* Inject the agent definition directly instead of reading AGENT.md from
|
|
107
|
+
* `workingDir`. Pass raw markdown (string) or a pre-parsed `ParsedAgent`.
|
|
108
|
+
* When provided, `storageEngine` is also required — the engine's
|
|
109
|
+
* `agentId` becomes the source of truth for partitioning, and the
|
|
110
|
+
* filesystem identity dance (`ensureAgentIdentity`) is skipped.
|
|
111
|
+
*/
|
|
112
|
+
agentDefinition?: string | ParsedAgent;
|
|
113
|
+
/**
|
|
114
|
+
* Pre-constructed storage engine. When provided, the harness will not
|
|
115
|
+
* create one internally. The engine's `agentId` is used wherever the
|
|
116
|
+
* harness today reads `parsedAgent.frontmatter.id`.
|
|
117
|
+
*/
|
|
118
|
+
storageEngine?: StorageEngine;
|
|
119
|
+
/**
|
|
120
|
+
* Inject a `PonchoConfig` object directly instead of importing
|
|
121
|
+
* `poncho.config.js` from `workingDir`. When provided, the disk-based
|
|
122
|
+
* loader is skipped. Downstream resolvers (`resolveMemoryConfig`,
|
|
123
|
+
* `resolveStateConfig`, etc.) run as today regardless of source.
|
|
124
|
+
*/
|
|
125
|
+
config?: PonchoConfig;
|
|
105
126
|
}
|
|
106
127
|
|
|
107
128
|
export interface HarnessRunOutput {
|
|
@@ -805,6 +826,7 @@ export class AgentHarness {
|
|
|
805
826
|
reminderStore?: ReminderStore;
|
|
806
827
|
secretsStore?: SecretsStore;
|
|
807
828
|
private loadedConfig?: PonchoConfig;
|
|
829
|
+
private readonly injectedConfig?: PonchoConfig;
|
|
808
830
|
private loadedSkills: SkillMetadata[] = [];
|
|
809
831
|
private skillFingerprint = "";
|
|
810
832
|
private lastSkillRefreshAt = 0;
|
|
@@ -823,6 +845,8 @@ export class AgentHarness {
|
|
|
823
845
|
|
|
824
846
|
private parsedAgent?: ParsedAgent;
|
|
825
847
|
private agentFileFingerprint = "";
|
|
848
|
+
private injectedAgentDefinition?: string | ParsedAgent;
|
|
849
|
+
private injectedStorageEngine = false;
|
|
826
850
|
private mcpBridge?: LocalMcpBridge;
|
|
827
851
|
private subagentManager?: SubagentManager;
|
|
828
852
|
private readonly archivedToolResultsByConversation = new Map<string, Record<string, ArchivedToolResult>>();
|
|
@@ -1011,6 +1035,19 @@ export class AgentHarness {
|
|
|
1011
1035
|
this.modelProviderInjected = !!options.modelProvider;
|
|
1012
1036
|
this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
|
|
1013
1037
|
this.uploadStore = options.uploadStore;
|
|
1038
|
+
this.injectedConfig = options.config;
|
|
1039
|
+
|
|
1040
|
+
if (options.agentDefinition !== undefined && options.storageEngine === undefined) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
"HarnessOptions.agentDefinition requires HarnessOptions.storageEngine — " +
|
|
1043
|
+
"construct a StorageEngine with the desired agentId and pass both.",
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
this.injectedAgentDefinition = options.agentDefinition;
|
|
1047
|
+
if (options.storageEngine) {
|
|
1048
|
+
this.storageEngine = options.storageEngine;
|
|
1049
|
+
this.injectedStorageEngine = true;
|
|
1050
|
+
}
|
|
1014
1051
|
|
|
1015
1052
|
if (options.toolDefinitions?.length) {
|
|
1016
1053
|
this.dispatcher.registerMany(options.toolDefinitions);
|
|
@@ -1508,6 +1545,11 @@ export class AgentHarness {
|
|
|
1508
1545
|
if (this.environment !== "development") {
|
|
1509
1546
|
return false;
|
|
1510
1547
|
}
|
|
1548
|
+
if (this.injectedAgentDefinition !== undefined) {
|
|
1549
|
+
// Caller owns the agent definition — re-instantiate the harness to
|
|
1550
|
+
// pick up changes rather than re-reading from disk.
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1511
1553
|
try {
|
|
1512
1554
|
const agentFilePath = resolve(this.workingDir, "AGENT.md");
|
|
1513
1555
|
const rawContent = await readFile(agentFilePath, "utf8");
|
|
@@ -1586,15 +1628,28 @@ export class AgentHarness {
|
|
|
1586
1628
|
}
|
|
1587
1629
|
|
|
1588
1630
|
async initialize(): Promise<void> {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1631
|
+
if (this.injectedAgentDefinition !== undefined) {
|
|
1632
|
+
this.parsedAgent = typeof this.injectedAgentDefinition === "string"
|
|
1633
|
+
? parseAgentMarkdown(this.injectedAgentDefinition)
|
|
1634
|
+
: this.injectedAgentDefinition;
|
|
1635
|
+
this.agentFileFingerprint = "";
|
|
1636
|
+
// The injected StorageEngine is the source of truth for agentId.
|
|
1637
|
+
// Mirror it onto frontmatter.id so existing downstream readers
|
|
1638
|
+
// (`frontmatter.id ?? frontmatter.name`) keep resolving correctly.
|
|
1639
|
+
if (this.storageEngine) {
|
|
1640
|
+
this.parsedAgent.frontmatter.id = this.storageEngine.agentId;
|
|
1641
|
+
}
|
|
1642
|
+
} else {
|
|
1643
|
+
const agentFilePath = resolve(this.workingDir, "AGENT.md");
|
|
1644
|
+
const agentRawContent = await readFile(agentFilePath, "utf8");
|
|
1645
|
+
this.parsedAgent = parseAgentMarkdown(agentRawContent);
|
|
1646
|
+
this.agentFileFingerprint = agentRawContent;
|
|
1647
|
+
const identity = await ensureAgentIdentity(this.workingDir);
|
|
1648
|
+
if (!this.parsedAgent.frontmatter.id) {
|
|
1649
|
+
this.parsedAgent.frontmatter.id = identity.id;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const config = this.injectedConfig ?? await loadPonchoConfig(this.workingDir);
|
|
1598
1653
|
this.loadedConfig = config;
|
|
1599
1654
|
this.registerConfiguredBuiltInTools(config);
|
|
1600
1655
|
const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
|
|
@@ -1612,15 +1667,23 @@ export class AgentHarness {
|
|
|
1612
1667
|
const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
|
|
1613
1668
|
|
|
1614
1669
|
// --- Unified Storage Engine ---
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1670
|
+
let engine: StorageEngine;
|
|
1671
|
+
if (this.injectedStorageEngine && this.storageEngine) {
|
|
1672
|
+
// Caller-constructed engine; assume already initialized or will be
|
|
1673
|
+
// initialized by them (initialize() is idempotent in current impls).
|
|
1674
|
+
engine = this.storageEngine;
|
|
1675
|
+
await engine.initialize();
|
|
1676
|
+
} else {
|
|
1677
|
+
const storageProvider = (config?.storage?.provider ?? "sqlite") as StorageProvider;
|
|
1678
|
+
engine = createStorageEngine({
|
|
1679
|
+
provider: storageProvider,
|
|
1680
|
+
workingDir: this.workingDir,
|
|
1681
|
+
agentId,
|
|
1682
|
+
urlEnv: config?.storage?.urlEnv,
|
|
1683
|
+
});
|
|
1684
|
+
await engine.initialize();
|
|
1685
|
+
this.storageEngine = engine;
|
|
1686
|
+
}
|
|
1624
1687
|
|
|
1625
1688
|
// --- Bash Environment Manager ---
|
|
1626
1689
|
const maxFileSize = config?.storage?.limits?.maxFileSize ?? 100 * 1024 * 1024; // 100MB
|