@poncho-ai/harness 0.48.0 → 0.50.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 +6 -6
- package/CHANGELOG.md +50 -0
- package/dist/index.d.ts +45 -6
- package/dist/index.js +66 -6
- package/dist/{isolate-VY35DGLM.js → isolate-BNQ6P3HI.js} +49 -3
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/harness.ts +8 -2
- package/src/index.ts +5 -1
- package/src/isolate/bindings.ts +14 -1
- package/src/isolate/polyfills.ts +39 -2
- package/src/memory.ts +9 -0
- package/src/vfs/poncho-fs-adapter.ts +108 -13
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.50.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/
|
|
12
|
-
[32mESM[39m [1mdist/
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mESM[39m [1mdist/isolate-BNQ6P3HI.js [22m[32m51.41 KB[39m
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m530.56 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 259ms
|
|
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 6896ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m89.28 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.50.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#129](https://github.com/cesr/poncho-ai/pull/129) [`85b8eec`](https://github.com/cesr/poncho-ai/commit/85b8eeca593b1043e2f7da01d681db6d32b1a969) Thanks [@cesr](https://github.com/cesr)! - harness: provider-backed VFS mounts + binary fetch bodies
|
|
8
|
+
|
|
9
|
+
Two additive isolate/VFS changes.
|
|
10
|
+
|
|
11
|
+
**`MountProvider` for `VirtualMount`.** A virtual mount can now be backed
|
|
12
|
+
by a custom data source instead of a local-disk directory. Set
|
|
13
|
+
`provider: { readdir, stat, readFileBuffer }` instead of `source` on a
|
|
14
|
+
`VirtualMount`. The adapter routes read operations through the provider
|
|
15
|
+
and rejects writes the same way it does for disk-backed mounts. Lets a
|
|
16
|
+
host expose database rows / object-store keys as a VFS subtree without
|
|
17
|
+
materialising them on disk (e.g. PonchOS exposing user uploads at
|
|
18
|
+
`/uploads`). `getAllPaths` advertises only the mount root for provider
|
|
19
|
+
mounts (deep listing would require sync IO over a remote backend);
|
|
20
|
+
shallow listing is sufficient for bash glob/find at the mount root.
|
|
21
|
+
|
|
22
|
+
**Binary `fetch()` bodies in `run_code`.** The isolate fetch polyfill
|
|
23
|
+
used to coerce `init.body` to a string before sending it to the
|
|
24
|
+
`__poncho_fetch` binding, so passing a `Uint8Array`, `ArrayBuffer`, or
|
|
25
|
+
`Blob` arrived server-side as `"1,2,3,..."` — every binary upload
|
|
26
|
+
(image-edit APIs, file uploads) was corrupted. The polyfill now
|
|
27
|
+
base64-encodes binary bodies with a new `bodyEncoding: "base64"` field
|
|
28
|
+
on the binding input; the built-in `createFetchBinding` decodes back to
|
|
29
|
+
raw bytes before fetching. Custom bindings that replace `__poncho_fetch`
|
|
30
|
+
should add the same decoding (cf. PonchOS `createSecretAwareFetchBinding`).
|
|
31
|
+
String bodies are unchanged.
|
|
32
|
+
|
|
33
|
+
## 0.49.0
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- [#127](https://github.com/cesr/poncho-ai/pull/127) [`87b40d9`](https://github.com/cesr/poncho-ai/commit/87b40d9d6cebba4ac646598d154a767a1d2f3551) Thanks [@cesr](https://github.com/cesr)! - harness: stop truncating main memory by default
|
|
38
|
+
|
|
39
|
+
Main memory injected into the system prompt was hard-truncated at 4000
|
|
40
|
+
characters with a `...[truncated]` marker. Silently dropping the tail of
|
|
41
|
+
a user's memory every turn is a footgun, so the **default is now no
|
|
42
|
+
truncation** — the full memory is injected.
|
|
43
|
+
|
|
44
|
+
New `MemoryConfig.maxPromptChars` (also settable via
|
|
45
|
+
`storage.memory.maxPromptChars`) lets a consumer opt back _into_ a cap
|
|
46
|
+
for prompt-cost control: set a positive number and content beyond it is
|
|
47
|
+
sliced with the `...[truncated]` marker as before.
|
|
48
|
+
|
|
49
|
+
Behavior change: consumers that relied on the implicit 4000-char cap
|
|
50
|
+
will now see full memory in the prompt. To restore the old behavior set
|
|
51
|
+
`maxPromptChars: 4000`.
|
|
52
|
+
|
|
3
53
|
## 0.48.0
|
|
4
54
|
|
|
5
55
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { LanguageModel } from 'ai';
|
|
|
2
2
|
import * as _poncho_ai_sdk from '@poncho-ai/sdk';
|
|
3
3
|
import { Message, ToolContext, ToolDefinition, JsonSchema, RunResult, AgentFailure, RunInput, AgentEvent, FileInput } from '@poncho-ai/sdk';
|
|
4
4
|
export { ToolDefinition, defineTool } from '@poncho-ai/sdk';
|
|
5
|
-
import { IFileSystem, BufferEncoding,
|
|
5
|
+
import { FsStat, IFileSystem, BufferEncoding, FileContent, MkdirOptions, RmOptions, CpOptions, Bash } from 'just-bash';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
|
|
8
8
|
interface AgentModelConfig {
|
|
@@ -341,6 +341,15 @@ interface MemoryConfig {
|
|
|
341
341
|
region?: string;
|
|
342
342
|
ttl?: number;
|
|
343
343
|
maxRecallConversations?: number;
|
|
344
|
+
/**
|
|
345
|
+
* Optional cap on the characters of main memory injected into the
|
|
346
|
+
* system prompt each turn. Default is **no cap** — the full memory is
|
|
347
|
+
* injected (silently truncating a user's memory every turn is a
|
|
348
|
+
* footgun). Set a positive number to opt into truncation for
|
|
349
|
+
* prompt-cost control; content beyond it is sliced with a
|
|
350
|
+
* `...[truncated]` marker.
|
|
351
|
+
*/
|
|
352
|
+
maxPromptChars?: number;
|
|
344
353
|
}
|
|
345
354
|
interface MemoryStore {
|
|
346
355
|
getMainMemory(): Promise<MainMemory>;
|
|
@@ -445,6 +454,7 @@ interface StorageConfig {
|
|
|
445
454
|
memory?: {
|
|
446
455
|
enabled?: boolean;
|
|
447
456
|
maxRecallConversations?: number;
|
|
457
|
+
maxPromptChars?: number;
|
|
448
458
|
};
|
|
449
459
|
limits?: {
|
|
450
460
|
maxFileSize?: number;
|
|
@@ -967,9 +977,34 @@ interface StorageEngine {
|
|
|
967
977
|
}
|
|
968
978
|
|
|
969
979
|
/**
|
|
970
|
-
* Read-only
|
|
971
|
-
*
|
|
972
|
-
*
|
|
980
|
+
* Read-only data source for a virtual mount whose contents don't live on
|
|
981
|
+
* local disk. Lets a host (e.g. PonchOS) back a VFS prefix with a database
|
|
982
|
+
* row set, object-store keys, or anything else expressible as
|
|
983
|
+
* `(tenantId, relative) -> bytes/listing/stat`.
|
|
984
|
+
*
|
|
985
|
+
* All three methods are read-only; writes through the adapter still throw
|
|
986
|
+
* `EROFS` for routed mounts the same way disk-backed mounts do (see
|
|
987
|
+
* `routeToMount` checks in writeFile/mkdir/rm/cp/mv/chmod/utimes/symlink/link).
|
|
988
|
+
* `relative` is the path within the mount; `""` means the mount root.
|
|
989
|
+
*/
|
|
990
|
+
interface MountProvider {
|
|
991
|
+
/** List entries directly under `relative`. The mount root is `""`. */
|
|
992
|
+
readdir(tenantId: string, relative: string): Promise<Array<{
|
|
993
|
+
name: string;
|
|
994
|
+
isFile: boolean;
|
|
995
|
+
isDirectory: boolean;
|
|
996
|
+
}>>;
|
|
997
|
+
/** Stat a single entry. Throw ENOENT-like for missing paths. */
|
|
998
|
+
stat(tenantId: string, relative: string): Promise<FsStat>;
|
|
999
|
+
/** Read raw bytes for a file entry. */
|
|
1000
|
+
readFileBuffer(tenantId: string, relative: string): Promise<Uint8Array>;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Read-only virtual mount mapping a VFS path prefix to either a local
|
|
1004
|
+
* filesystem directory (`source`) OR a custom provider (`provider`).
|
|
1005
|
+
* Exactly one of the two must be set. All read operations under the prefix
|
|
1006
|
+
* resolve via the chosen backend; writes throw. The prefix is normalised
|
|
1007
|
+
* internally to end with "/".
|
|
973
1008
|
*/
|
|
974
1009
|
interface VirtualMount {
|
|
975
1010
|
/** VFS prefix, e.g. "/system/". Leading slash required; trailing slash
|
|
@@ -977,7 +1012,11 @@ interface VirtualMount {
|
|
|
977
1012
|
* but no validation is enforced here. */
|
|
978
1013
|
prefix: string;
|
|
979
1014
|
/** Absolute local FS path to serve from, e.g. "/srv/poncho/system". */
|
|
980
|
-
source
|
|
1015
|
+
source?: string;
|
|
1016
|
+
/** Custom backend serving reads for this mount. Mutually exclusive with
|
|
1017
|
+
* `source` — set exactly one. Use when the data doesn't live on local
|
|
1018
|
+
* disk (e.g. database-backed upload listings). */
|
|
1019
|
+
provider?: MountProvider;
|
|
981
1020
|
}
|
|
982
1021
|
declare class PonchoFsAdapter implements IFileSystem {
|
|
983
1022
|
private engine;
|
|
@@ -2099,4 +2138,4 @@ interface RunConversationTurnResult {
|
|
|
2099
2138
|
}
|
|
2100
2139
|
declare const runConversationTurn: (opts: RunConversationTurnOpts) => Promise<RunConversationTurnResult>;
|
|
2101
2140
|
|
|
2102
|
-
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type NetworkConfig, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
|
2141
|
+
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentSpawnResult, type SubagentSummary, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
package/dist/index.js
CHANGED
|
@@ -544,7 +544,8 @@ var resolveMemoryConfig = (config) => {
|
|
|
544
544
|
table: config.storage.table,
|
|
545
545
|
region: config.storage.region,
|
|
546
546
|
ttl: resolveTtl(config.storage.ttl, "memory"),
|
|
547
|
-
maxRecallConversations: config.storage.memory?.maxRecallConversations ?? config.memory?.maxRecallConversations
|
|
547
|
+
maxRecallConversations: config.storage.memory?.maxRecallConversations ?? config.memory?.maxRecallConversations,
|
|
548
|
+
maxPromptChars: config.storage.memory?.maxPromptChars ?? config.memory?.maxPromptChars
|
|
548
549
|
};
|
|
549
550
|
}
|
|
550
551
|
return config?.memory;
|
|
@@ -4690,10 +4691,18 @@ var PonchoFsAdapter = class {
|
|
|
4690
4691
|
this.limits = limits;
|
|
4691
4692
|
this.mounts = mounts.map((m) => {
|
|
4692
4693
|
const prefix = m.prefix.endsWith("/") ? m.prefix : m.prefix + "/";
|
|
4694
|
+
const hasSource = typeof m.source === "string";
|
|
4695
|
+
const hasProvider = m.provider != null;
|
|
4696
|
+
if (hasSource === hasProvider) {
|
|
4697
|
+
throw new Error(
|
|
4698
|
+
`VirtualMount '${m.prefix}': set exactly one of 'source' or 'provider'`
|
|
4699
|
+
);
|
|
4700
|
+
}
|
|
4693
4701
|
return {
|
|
4694
4702
|
prefix,
|
|
4695
4703
|
prefixNoSlash: prefix.slice(0, -1),
|
|
4696
|
-
source: m.source.replace(/\/+$/, "")
|
|
4704
|
+
source: hasSource ? m.source.replace(/\/+$/, "") : void 0,
|
|
4705
|
+
provider: m.provider
|
|
4697
4706
|
};
|
|
4698
4707
|
});
|
|
4699
4708
|
}
|
|
@@ -4756,6 +4765,10 @@ var PonchoFsAdapter = class {
|
|
|
4756
4765
|
const np = normalize(path);
|
|
4757
4766
|
const route = this.routeToMount(np);
|
|
4758
4767
|
if (route) {
|
|
4768
|
+
if (route.mount.provider) {
|
|
4769
|
+
const buf3 = await route.mount.provider.readFileBuffer(this.tenantId, route.relative);
|
|
4770
|
+
return new TextDecoder().decode(buf3);
|
|
4771
|
+
}
|
|
4759
4772
|
const buf2 = await nodeFs.readFile(this.toLocal(route.mount, route.relative));
|
|
4760
4773
|
return buf2.toString("utf8");
|
|
4761
4774
|
}
|
|
@@ -4766,6 +4779,9 @@ var PonchoFsAdapter = class {
|
|
|
4766
4779
|
const np = normalize(path);
|
|
4767
4780
|
const route = this.routeToMount(np);
|
|
4768
4781
|
if (route) {
|
|
4782
|
+
if (route.mount.provider) {
|
|
4783
|
+
return route.mount.provider.readFileBuffer(this.tenantId, route.relative);
|
|
4784
|
+
}
|
|
4769
4785
|
const buf = await nodeFs.readFile(this.toLocal(route.mount, route.relative));
|
|
4770
4786
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
4771
4787
|
}
|
|
@@ -4775,6 +4791,14 @@ var PonchoFsAdapter = class {
|
|
|
4775
4791
|
const np = normalize(path);
|
|
4776
4792
|
const route = this.routeToMount(np);
|
|
4777
4793
|
if (route) {
|
|
4794
|
+
if (route.mount.provider) {
|
|
4795
|
+
try {
|
|
4796
|
+
await route.mount.provider.stat(this.tenantId, route.relative);
|
|
4797
|
+
return true;
|
|
4798
|
+
} catch {
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4778
4802
|
try {
|
|
4779
4803
|
await nodeFs.access(this.toLocal(route.mount, route.relative));
|
|
4780
4804
|
return true;
|
|
@@ -4790,6 +4814,13 @@ var PonchoFsAdapter = class {
|
|
|
4790
4814
|
const np = normalize(path);
|
|
4791
4815
|
const route = this.routeToMount(np);
|
|
4792
4816
|
if (route) {
|
|
4817
|
+
if (route.mount.provider) {
|
|
4818
|
+
try {
|
|
4819
|
+
return await route.mount.provider.stat(this.tenantId, route.relative);
|
|
4820
|
+
} catch {
|
|
4821
|
+
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4793
4824
|
try {
|
|
4794
4825
|
const s2 = await nodeFs.stat(this.toLocal(route.mount, route.relative));
|
|
4795
4826
|
return this.toFsStat(s2);
|
|
@@ -4815,6 +4846,10 @@ var PonchoFsAdapter = class {
|
|
|
4815
4846
|
const np = normalize(path);
|
|
4816
4847
|
const route = this.routeToMount(np);
|
|
4817
4848
|
if (route) {
|
|
4849
|
+
if (route.mount.provider) {
|
|
4850
|
+
const entries = await route.mount.provider.readdir(this.tenantId, route.relative);
|
|
4851
|
+
return entries.map((e) => e.name);
|
|
4852
|
+
}
|
|
4818
4853
|
return nodeFs.readdir(this.toLocal(route.mount, route.relative));
|
|
4819
4854
|
}
|
|
4820
4855
|
let engineNames = [];
|
|
@@ -4833,6 +4868,15 @@ var PonchoFsAdapter = class {
|
|
|
4833
4868
|
const np = normalize(path);
|
|
4834
4869
|
const route = this.routeToMount(np);
|
|
4835
4870
|
if (route) {
|
|
4871
|
+
if (route.mount.provider) {
|
|
4872
|
+
const entries2 = await route.mount.provider.readdir(this.tenantId, route.relative);
|
|
4873
|
+
return entries2.map((e) => ({
|
|
4874
|
+
name: e.name,
|
|
4875
|
+
isFile: e.isFile,
|
|
4876
|
+
isDirectory: e.isDirectory,
|
|
4877
|
+
isSymbolicLink: false
|
|
4878
|
+
}));
|
|
4879
|
+
}
|
|
4836
4880
|
const entries = await nodeFs.readdir(this.toLocal(route.mount, route.relative), { withFileTypes: true });
|
|
4837
4881
|
return entries.map((e) => ({
|
|
4838
4882
|
name: e.name,
|
|
@@ -4938,8 +4982,12 @@ var PonchoFsAdapter = class {
|
|
|
4938
4982
|
const np = normalize(path);
|
|
4939
4983
|
const route = this.routeToMount(np);
|
|
4940
4984
|
if (route) {
|
|
4985
|
+
if (route.mount.provider) {
|
|
4986
|
+
return np;
|
|
4987
|
+
}
|
|
4941
4988
|
const localResolved = await nodeFs.realpath(this.toLocal(route.mount, route.relative));
|
|
4942
|
-
const
|
|
4989
|
+
const source = route.mount.source;
|
|
4990
|
+
const localRoot = await nodeFs.realpath(source).catch(() => source);
|
|
4943
4991
|
if (localResolved === localRoot) return route.mount.prefixNoSlash;
|
|
4944
4992
|
if (localResolved.startsWith(localRoot + nodePath.sep)) {
|
|
4945
4993
|
const rel = localResolved.slice(localRoot.length + 1).split(nodePath.sep).join("/");
|
|
@@ -4965,6 +5013,7 @@ var PonchoFsAdapter = class {
|
|
|
4965
5013
|
const out = new Set(enginePaths);
|
|
4966
5014
|
for (const m of this.mounts) {
|
|
4967
5015
|
out.add(m.prefixNoSlash);
|
|
5016
|
+
if (m.provider) continue;
|
|
4968
5017
|
try {
|
|
4969
5018
|
const stack = [
|
|
4970
5019
|
{ abs: m.source, vfs: m.prefixNoSlash }
|
|
@@ -5018,6 +5067,9 @@ var PonchoFsAdapter = class {
|
|
|
5018
5067
|
const np = normalize(path);
|
|
5019
5068
|
const route = this.routeToMount(np);
|
|
5020
5069
|
if (route) {
|
|
5070
|
+
if (route.mount.provider) {
|
|
5071
|
+
throw new Error(`EINVAL: not a symbolic link, readlink '${path}'`);
|
|
5072
|
+
}
|
|
5021
5073
|
return nodeFs.readlink(this.toLocal(route.mount, route.relative));
|
|
5022
5074
|
}
|
|
5023
5075
|
return this.engine.vfs.readlink(this.tenantId, np);
|
|
@@ -5026,6 +5078,13 @@ var PonchoFsAdapter = class {
|
|
|
5026
5078
|
const np = normalize(path);
|
|
5027
5079
|
const route = this.routeToMount(np);
|
|
5028
5080
|
if (route) {
|
|
5081
|
+
if (route.mount.provider) {
|
|
5082
|
+
try {
|
|
5083
|
+
return await route.mount.provider.stat(this.tenantId, route.relative);
|
|
5084
|
+
} catch {
|
|
5085
|
+
throw new Error(`ENOENT: no such file or directory, lstat '${path}'`);
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5029
5088
|
try {
|
|
5030
5089
|
const s2 = await nodeFs.lstat(this.toLocal(route.mount, route.relative));
|
|
5031
5090
|
return this.toFsStat(s2);
|
|
@@ -9891,7 +9950,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
9891
9950
|
this.registerIfMissing(createEditFileTool(getFs));
|
|
9892
9951
|
this.registerIfMissing(createWriteFileTool(getFs));
|
|
9893
9952
|
if (config?.isolate) {
|
|
9894
|
-
const { createRunCodeTool, buildRunCodeDescription, bundleLibraries } = await import("./isolate-
|
|
9953
|
+
const { createRunCodeTool, buildRunCodeDescription, bundleLibraries } = await import("./isolate-BNQ6P3HI.js");
|
|
9895
9954
|
let libraryPreamble = null;
|
|
9896
9955
|
if (config.isolate.libraries?.length) {
|
|
9897
9956
|
libraryPreamble = await bundleLibraries(config.isolate.libraries, this.workingDir);
|
|
@@ -10233,7 +10292,8 @@ Browser sessions (cookies, localStorage, login state) are automatically saved an
|
|
|
10233
10292
|
### Tabs and resources
|
|
10234
10293
|
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.` : "";
|
|
10235
10294
|
const mainMemory = await memoryPromise;
|
|
10236
|
-
const
|
|
10295
|
+
const memCap = this.memoryConfig?.maxPromptChars ?? 0;
|
|
10296
|
+
const boundedMainMemory = mainMemory && memCap > 0 && mainMemory.content.length > memCap ? `${mainMemory.content.slice(0, memCap)}
|
|
10237
10297
|
...[truncated]` : mainMemory?.content;
|
|
10238
10298
|
const memoryContext = boundedMainMemory && boundedMainMemory.trim().length > 0 ? `
|
|
10239
10299
|
## Persistent Memory
|
|
@@ -10266,7 +10326,7 @@ Examples:${this.environment !== "production" ? `
|
|
|
10266
10326
|
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.` : "";
|
|
10267
10327
|
let isolateContext = "";
|
|
10268
10328
|
if (this.loadedConfig?.isolate && this.dispatcher.get("run_code")) {
|
|
10269
|
-
const { generateIsolateTypeStubs } = await import("./isolate-
|
|
10329
|
+
const { generateIsolateTypeStubs } = await import("./isolate-BNQ6P3HI.js");
|
|
10270
10330
|
const typeStubs = generateIsolateTypeStubs(this.loadedConfig.isolate);
|
|
10271
10331
|
isolateContext = `
|
|
10272
10332
|
|
|
@@ -324,6 +324,11 @@ function createFetchBinding(allowedDomains, network) {
|
|
|
324
324
|
method: { type: "string" },
|
|
325
325
|
headers: { type: "object", additionalProperties: { type: "string" } },
|
|
326
326
|
body: { type: "string" },
|
|
327
|
+
// "base64" => `body` is base64-encoded raw bytes. The polyfill sets
|
|
328
|
+
// this when init.body is a Uint8Array/ArrayBuffer/Blob, so binary
|
|
329
|
+
// uploads (image-edit APIs, file uploads) reach the server intact
|
|
330
|
+
// instead of being mangled by String(...) coercion.
|
|
331
|
+
bodyEncoding: { type: "string", enum: ["base64"] },
|
|
327
332
|
binary: { type: "boolean" }
|
|
328
333
|
},
|
|
329
334
|
required: ["url"]
|
|
@@ -335,10 +340,14 @@ function createFetchBinding(allowedDomains, network) {
|
|
|
335
340
|
`Fetch blocked: domain "${url.hostname}" is not in the allowed list [${allowedDomains.join(", ")}]`
|
|
336
341
|
);
|
|
337
342
|
}
|
|
343
|
+
const rawBody = input.body;
|
|
344
|
+
const reqBody = rawBody !== void 0 && input.bodyEncoding === "base64" ? new Uint8Array(Buffer.from(rawBody, "base64")) : rawBody;
|
|
338
345
|
const resp = await fetch(input.url, {
|
|
339
346
|
method: input.method ?? "GET",
|
|
340
347
|
headers: input.headers ?? void 0,
|
|
341
|
-
|
|
348
|
+
// Cast: Node's undici fetch accepts Uint8Array at runtime, but the
|
|
349
|
+
// BodyInit type in this lib version doesn't list it.
|
|
350
|
+
body: reqBody,
|
|
342
351
|
redirect: "follow"
|
|
343
352
|
});
|
|
344
353
|
const headers = {};
|
|
@@ -846,6 +855,36 @@ var POLYFILL_FETCH = `
|
|
|
846
855
|
return arr;
|
|
847
856
|
}
|
|
848
857
|
|
|
858
|
+
function _fetchUint8ToB64(u8) {
|
|
859
|
+
let bin = "";
|
|
860
|
+
for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i]);
|
|
861
|
+
return btoa(bin);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Normalise the init.body into { body, bodyEncoding } the binding accepts.
|
|
865
|
+
// Strings go through as text. Binary inputs (Uint8Array / ArrayBuffer /
|
|
866
|
+
// typed-array views / Blob) are base64-encoded with bodyEncoding="base64"
|
|
867
|
+
// so the host decodes back to the exact bytes before fetch() \u2014 without
|
|
868
|
+
// this branch, String(uint8Array) gave "1,2,3,..." and corrupted every
|
|
869
|
+
// binary upload (image-edit APIs, file uploads, etc.).
|
|
870
|
+
function _fetchEncodeBody(raw) {
|
|
871
|
+
if (raw == null) return { body: undefined };
|
|
872
|
+
if (typeof raw === "string") return { body: raw };
|
|
873
|
+
if (raw instanceof ArrayBuffer) {
|
|
874
|
+
return { body: _fetchUint8ToB64(new Uint8Array(raw)), bodyEncoding: "base64" };
|
|
875
|
+
}
|
|
876
|
+
if (ArrayBuffer.isView(raw)) {
|
|
877
|
+
const u8 = raw instanceof Uint8Array
|
|
878
|
+
? raw
|
|
879
|
+
: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
880
|
+
return { body: _fetchUint8ToB64(u8), bodyEncoding: "base64" };
|
|
881
|
+
}
|
|
882
|
+
if (typeof Blob !== "undefined" && raw instanceof Blob && raw._data) {
|
|
883
|
+
return { body: _fetchUint8ToB64(raw._data), bodyEncoding: "base64" };
|
|
884
|
+
}
|
|
885
|
+
return { body: String(raw) };
|
|
886
|
+
}
|
|
887
|
+
|
|
849
888
|
globalThis.fetch = async function(input, init) {
|
|
850
889
|
const url = typeof input === "string" ? input : (input?.url || String(input));
|
|
851
890
|
const method = init?.method || "GET";
|
|
@@ -856,10 +895,17 @@ var POLYFILL_FETCH = `
|
|
|
856
895
|
: Object.entries(init.headers);
|
|
857
896
|
for (const [k, v] of entries) headers[k] = String(v);
|
|
858
897
|
}
|
|
859
|
-
const
|
|
898
|
+
const encoded = _fetchEncodeBody(init?.body);
|
|
860
899
|
|
|
861
900
|
// Always fetch as binary to preserve data integrity
|
|
862
|
-
const result = await __poncho_fetch({
|
|
901
|
+
const result = await __poncho_fetch({
|
|
902
|
+
url,
|
|
903
|
+
method,
|
|
904
|
+
headers,
|
|
905
|
+
body: encoded.body,
|
|
906
|
+
bodyEncoding: encoded.bodyEncoding,
|
|
907
|
+
binary: true,
|
|
908
|
+
});
|
|
863
909
|
return new Response(result, true);
|
|
864
910
|
};
|
|
865
911
|
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface StorageConfig {
|
|
|
21
21
|
memory?: {
|
|
22
22
|
enabled?: boolean;
|
|
23
23
|
maxRecallConversations?: number;
|
|
24
|
+
maxPromptChars?: number;
|
|
24
25
|
};
|
|
25
26
|
limits?: {
|
|
26
27
|
maxFileSize?: number;
|
|
@@ -335,6 +336,9 @@ export const resolveMemoryConfig = (
|
|
|
335
336
|
maxRecallConversations:
|
|
336
337
|
config.storage.memory?.maxRecallConversations ??
|
|
337
338
|
config.memory?.maxRecallConversations,
|
|
339
|
+
maxPromptChars:
|
|
340
|
+
config.storage.memory?.maxPromptChars ??
|
|
341
|
+
config.memory?.maxPromptChars,
|
|
338
342
|
};
|
|
339
343
|
}
|
|
340
344
|
return config?.memory;
|
package/src/harness.ts
CHANGED
|
@@ -2184,9 +2184,15 @@ Browser sessions (cookies, localStorage, login state) are automatically saved an
|
|
|
2184
2184
|
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.`
|
|
2185
2185
|
: "";
|
|
2186
2186
|
const mainMemory = await memoryPromise;
|
|
2187
|
+
// Main memory is injected in full by default — silently dropping the
|
|
2188
|
+
// tail of a user's memory every turn is a footgun. Set
|
|
2189
|
+
// `maxPromptChars` to a positive number to opt into a cap (e.g. for
|
|
2190
|
+
// prompt-cost control); content beyond it is sliced with a
|
|
2191
|
+
// `...[truncated]` marker.
|
|
2192
|
+
const memCap = this.memoryConfig?.maxPromptChars ?? 0;
|
|
2187
2193
|
const boundedMainMemory =
|
|
2188
|
-
mainMemory && mainMemory.content.length >
|
|
2189
|
-
? `${mainMemory.content.slice(0,
|
|
2194
|
+
mainMemory && memCap > 0 && mainMemory.content.length > memCap
|
|
2195
|
+
? `${mainMemory.content.slice(0, memCap)}\n...[truncated]`
|
|
2190
2196
|
: mainMemory?.content;
|
|
2191
2197
|
const memoryContext =
|
|
2192
2198
|
boundedMainMemory && boundedMainMemory.trim().length > 0
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,11 @@ export * from "./telemetry.js";
|
|
|
21
21
|
export * from "./secrets-store.js";
|
|
22
22
|
export * from "./storage/index.js";
|
|
23
23
|
export * from "./storage/store-adapters.js";
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
PonchoFsAdapter,
|
|
26
|
+
type VirtualMount,
|
|
27
|
+
type MountProvider,
|
|
28
|
+
} from "./vfs/poncho-fs-adapter.js";
|
|
25
29
|
export { BashEnvironmentManager } from "./vfs/bash-manager.js";
|
|
26
30
|
export { createBashTool } from "./vfs/bash-tool.js";
|
|
27
31
|
export * from "./tenant-token.js";
|
package/src/isolate/bindings.ts
CHANGED
|
@@ -161,6 +161,11 @@ export function createFetchBinding(
|
|
|
161
161
|
method: { type: "string" },
|
|
162
162
|
headers: { type: "object", additionalProperties: { type: "string" } },
|
|
163
163
|
body: { type: "string" },
|
|
164
|
+
// "base64" => `body` is base64-encoded raw bytes. The polyfill sets
|
|
165
|
+
// this when init.body is a Uint8Array/ArrayBuffer/Blob, so binary
|
|
166
|
+
// uploads (image-edit APIs, file uploads) reach the server intact
|
|
167
|
+
// instead of being mangled by String(...) coercion.
|
|
168
|
+
bodyEncoding: { type: "string", enum: ["base64"] },
|
|
164
169
|
binary: { type: "boolean" },
|
|
165
170
|
},
|
|
166
171
|
required: ["url"],
|
|
@@ -173,10 +178,18 @@ export function createFetchBinding(
|
|
|
173
178
|
);
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
const rawBody = input.body as string | undefined;
|
|
182
|
+
const reqBody: string | Uint8Array | undefined =
|
|
183
|
+
rawBody !== undefined && input.bodyEncoding === "base64"
|
|
184
|
+
? new Uint8Array(Buffer.from(rawBody, "base64"))
|
|
185
|
+
: rawBody;
|
|
186
|
+
|
|
176
187
|
const resp = await fetch(input.url as string, {
|
|
177
188
|
method: (input.method as string) ?? "GET",
|
|
178
189
|
headers: (input.headers as Record<string, string>) ?? undefined,
|
|
179
|
-
|
|
190
|
+
// Cast: Node's undici fetch accepts Uint8Array at runtime, but the
|
|
191
|
+
// BodyInit type in this lib version doesn't list it.
|
|
192
|
+
body: reqBody as unknown as BodyInit | undefined,
|
|
180
193
|
redirect: "follow",
|
|
181
194
|
});
|
|
182
195
|
|
package/src/isolate/polyfills.ts
CHANGED
|
@@ -532,6 +532,36 @@ const POLYFILL_FETCH = `
|
|
|
532
532
|
return arr;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
function _fetchUint8ToB64(u8) {
|
|
536
|
+
let bin = "";
|
|
537
|
+
for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i]);
|
|
538
|
+
return btoa(bin);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Normalise the init.body into { body, bodyEncoding } the binding accepts.
|
|
542
|
+
// Strings go through as text. Binary inputs (Uint8Array / ArrayBuffer /
|
|
543
|
+
// typed-array views / Blob) are base64-encoded with bodyEncoding="base64"
|
|
544
|
+
// so the host decodes back to the exact bytes before fetch() — without
|
|
545
|
+
// this branch, String(uint8Array) gave "1,2,3,..." and corrupted every
|
|
546
|
+
// binary upload (image-edit APIs, file uploads, etc.).
|
|
547
|
+
function _fetchEncodeBody(raw) {
|
|
548
|
+
if (raw == null) return { body: undefined };
|
|
549
|
+
if (typeof raw === "string") return { body: raw };
|
|
550
|
+
if (raw instanceof ArrayBuffer) {
|
|
551
|
+
return { body: _fetchUint8ToB64(new Uint8Array(raw)), bodyEncoding: "base64" };
|
|
552
|
+
}
|
|
553
|
+
if (ArrayBuffer.isView(raw)) {
|
|
554
|
+
const u8 = raw instanceof Uint8Array
|
|
555
|
+
? raw
|
|
556
|
+
: new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
557
|
+
return { body: _fetchUint8ToB64(u8), bodyEncoding: "base64" };
|
|
558
|
+
}
|
|
559
|
+
if (typeof Blob !== "undefined" && raw instanceof Blob && raw._data) {
|
|
560
|
+
return { body: _fetchUint8ToB64(raw._data), bodyEncoding: "base64" };
|
|
561
|
+
}
|
|
562
|
+
return { body: String(raw) };
|
|
563
|
+
}
|
|
564
|
+
|
|
535
565
|
globalThis.fetch = async function(input, init) {
|
|
536
566
|
const url = typeof input === "string" ? input : (input?.url || String(input));
|
|
537
567
|
const method = init?.method || "GET";
|
|
@@ -542,10 +572,17 @@ const POLYFILL_FETCH = `
|
|
|
542
572
|
: Object.entries(init.headers);
|
|
543
573
|
for (const [k, v] of entries) headers[k] = String(v);
|
|
544
574
|
}
|
|
545
|
-
const
|
|
575
|
+
const encoded = _fetchEncodeBody(init?.body);
|
|
546
576
|
|
|
547
577
|
// Always fetch as binary to preserve data integrity
|
|
548
|
-
const result = await __poncho_fetch({
|
|
578
|
+
const result = await __poncho_fetch({
|
|
579
|
+
url,
|
|
580
|
+
method,
|
|
581
|
+
headers,
|
|
582
|
+
body: encoded.body,
|
|
583
|
+
bodyEncoding: encoded.bodyEncoding,
|
|
584
|
+
binary: true,
|
|
585
|
+
});
|
|
549
586
|
return new Response(result, true);
|
|
550
587
|
};
|
|
551
588
|
|
package/src/memory.ts
CHANGED
|
@@ -15,6 +15,15 @@ export interface MemoryConfig {
|
|
|
15
15
|
region?: string;
|
|
16
16
|
ttl?: number;
|
|
17
17
|
maxRecallConversations?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Optional cap on the characters of main memory injected into the
|
|
20
|
+
* system prompt each turn. Default is **no cap** — the full memory is
|
|
21
|
+
* injected (silently truncating a user's memory every turn is a
|
|
22
|
+
* footgun). Set a positive number to opt into truncation for
|
|
23
|
+
* prompt-cost control; content beyond it is sliced with a
|
|
24
|
+
* `...[truncated]` marker.
|
|
25
|
+
*/
|
|
26
|
+
maxPromptChars?: number;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export interface MemoryStore {
|
|
@@ -85,9 +85,34 @@ const normalize = (path: string): string => {
|
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* Read-only
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* Read-only data source for a virtual mount whose contents don't live on
|
|
89
|
+
* local disk. Lets a host (e.g. PonchOS) back a VFS prefix with a database
|
|
90
|
+
* row set, object-store keys, or anything else expressible as
|
|
91
|
+
* `(tenantId, relative) -> bytes/listing/stat`.
|
|
92
|
+
*
|
|
93
|
+
* All three methods are read-only; writes through the adapter still throw
|
|
94
|
+
* `EROFS` for routed mounts the same way disk-backed mounts do (see
|
|
95
|
+
* `routeToMount` checks in writeFile/mkdir/rm/cp/mv/chmod/utimes/symlink/link).
|
|
96
|
+
* `relative` is the path within the mount; `""` means the mount root.
|
|
97
|
+
*/
|
|
98
|
+
export interface MountProvider {
|
|
99
|
+
/** List entries directly under `relative`. The mount root is `""`. */
|
|
100
|
+
readdir(
|
|
101
|
+
tenantId: string,
|
|
102
|
+
relative: string,
|
|
103
|
+
): Promise<Array<{ name: string; isFile: boolean; isDirectory: boolean }>>;
|
|
104
|
+
/** Stat a single entry. Throw ENOENT-like for missing paths. */
|
|
105
|
+
stat(tenantId: string, relative: string): Promise<FsStat>;
|
|
106
|
+
/** Read raw bytes for a file entry. */
|
|
107
|
+
readFileBuffer(tenantId: string, relative: string): Promise<Uint8Array>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read-only virtual mount mapping a VFS path prefix to either a local
|
|
112
|
+
* filesystem directory (`source`) OR a custom provider (`provider`).
|
|
113
|
+
* Exactly one of the two must be set. All read operations under the prefix
|
|
114
|
+
* resolve via the chosen backend; writes throw. The prefix is normalised
|
|
115
|
+
* internally to end with "/".
|
|
91
116
|
*/
|
|
92
117
|
export interface VirtualMount {
|
|
93
118
|
/** VFS prefix, e.g. "/system/". Leading slash required; trailing slash
|
|
@@ -95,15 +120,20 @@ export interface VirtualMount {
|
|
|
95
120
|
* but no validation is enforced here. */
|
|
96
121
|
prefix: string;
|
|
97
122
|
/** Absolute local FS path to serve from, e.g. "/srv/poncho/system". */
|
|
98
|
-
source
|
|
123
|
+
source?: string;
|
|
124
|
+
/** Custom backend serving reads for this mount. Mutually exclusive with
|
|
125
|
+
* `source` — set exactly one. Use when the data doesn't live on local
|
|
126
|
+
* disk (e.g. database-backed upload listings). */
|
|
127
|
+
provider?: MountProvider;
|
|
99
128
|
}
|
|
100
129
|
|
|
101
|
-
/** Internal normalised form: prefix always ends with "/", source
|
|
102
|
-
* trailing slash. */
|
|
130
|
+
/** Internal normalised form: prefix always ends with "/", source (when set)
|
|
131
|
+
* has no trailing slash. Exactly one of source/provider is populated. */
|
|
103
132
|
interface NormalisedMount {
|
|
104
133
|
prefix: string;
|
|
105
134
|
prefixNoSlash: string;
|
|
106
|
-
source
|
|
135
|
+
source?: string;
|
|
136
|
+
provider?: MountProvider;
|
|
107
137
|
}
|
|
108
138
|
|
|
109
139
|
const READ_ONLY_ERROR = (path: string, op: string): Error =>
|
|
@@ -120,10 +150,18 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
120
150
|
) {
|
|
121
151
|
this.mounts = mounts.map((m) => {
|
|
122
152
|
const prefix = m.prefix.endsWith("/") ? m.prefix : m.prefix + "/";
|
|
153
|
+
const hasSource = typeof m.source === "string";
|
|
154
|
+
const hasProvider = m.provider != null;
|
|
155
|
+
if (hasSource === hasProvider) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`VirtualMount '${m.prefix}': set exactly one of 'source' or 'provider'`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
123
160
|
return {
|
|
124
161
|
prefix,
|
|
125
162
|
prefixNoSlash: prefix.slice(0, -1),
|
|
126
|
-
source: m.source
|
|
163
|
+
source: hasSource ? m.source!.replace(/\/+$/, "") : undefined,
|
|
164
|
+
provider: m.provider,
|
|
127
165
|
};
|
|
128
166
|
});
|
|
129
167
|
}
|
|
@@ -157,8 +195,9 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
157
195
|
}
|
|
158
196
|
|
|
159
197
|
private toLocal(mount: NormalisedMount, relative: string): string {
|
|
160
|
-
//
|
|
161
|
-
|
|
198
|
+
// Only callable for disk-backed mounts; provider mounts never call this.
|
|
199
|
+
// nodePath.join handles empty relative -> source dir.
|
|
200
|
+
return nodePath.join(mount.source!, relative);
|
|
162
201
|
}
|
|
163
202
|
|
|
164
203
|
/** Build an FsStat from a node fs.Stats. */
|
|
@@ -193,6 +232,10 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
193
232
|
const np = normalize(path);
|
|
194
233
|
const route = this.routeToMount(np);
|
|
195
234
|
if (route) {
|
|
235
|
+
if (route.mount.provider) {
|
|
236
|
+
const buf = await route.mount.provider.readFileBuffer(this.tenantId, route.relative);
|
|
237
|
+
return new TextDecoder().decode(buf);
|
|
238
|
+
}
|
|
196
239
|
const buf = await nodeFs.readFile(this.toLocal(route.mount, route.relative));
|
|
197
240
|
return buf.toString("utf8");
|
|
198
241
|
}
|
|
@@ -204,6 +247,9 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
204
247
|
const np = normalize(path);
|
|
205
248
|
const route = this.routeToMount(np);
|
|
206
249
|
if (route) {
|
|
250
|
+
if (route.mount.provider) {
|
|
251
|
+
return route.mount.provider.readFileBuffer(this.tenantId, route.relative);
|
|
252
|
+
}
|
|
207
253
|
const buf = await nodeFs.readFile(this.toLocal(route.mount, route.relative));
|
|
208
254
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
209
255
|
}
|
|
@@ -214,6 +260,14 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
214
260
|
const np = normalize(path);
|
|
215
261
|
const route = this.routeToMount(np);
|
|
216
262
|
if (route) {
|
|
263
|
+
if (route.mount.provider) {
|
|
264
|
+
try {
|
|
265
|
+
await route.mount.provider.stat(this.tenantId, route.relative);
|
|
266
|
+
return true;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
217
271
|
try {
|
|
218
272
|
await nodeFs.access(this.toLocal(route.mount, route.relative));
|
|
219
273
|
return true;
|
|
@@ -232,6 +286,13 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
232
286
|
const np = normalize(path);
|
|
233
287
|
const route = this.routeToMount(np);
|
|
234
288
|
if (route) {
|
|
289
|
+
if (route.mount.provider) {
|
|
290
|
+
try {
|
|
291
|
+
return await route.mount.provider.stat(this.tenantId, route.relative);
|
|
292
|
+
} catch {
|
|
293
|
+
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
235
296
|
try {
|
|
236
297
|
const s = await nodeFs.stat(this.toLocal(route.mount, route.relative));
|
|
237
298
|
return this.toFsStat(s);
|
|
@@ -259,6 +320,10 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
259
320
|
const np = normalize(path);
|
|
260
321
|
const route = this.routeToMount(np);
|
|
261
322
|
if (route) {
|
|
323
|
+
if (route.mount.provider) {
|
|
324
|
+
const entries = await route.mount.provider.readdir(this.tenantId, route.relative);
|
|
325
|
+
return entries.map((e) => e.name);
|
|
326
|
+
}
|
|
262
327
|
return nodeFs.readdir(this.toLocal(route.mount, route.relative));
|
|
263
328
|
}
|
|
264
329
|
// Engine-backed read; also inject any mount-root segments whose parent
|
|
@@ -282,6 +347,15 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
282
347
|
const np = normalize(path);
|
|
283
348
|
const route = this.routeToMount(np);
|
|
284
349
|
if (route) {
|
|
350
|
+
if (route.mount.provider) {
|
|
351
|
+
const entries = await route.mount.provider.readdir(this.tenantId, route.relative);
|
|
352
|
+
return entries.map((e) => ({
|
|
353
|
+
name: e.name,
|
|
354
|
+
isFile: e.isFile,
|
|
355
|
+
isDirectory: e.isDirectory,
|
|
356
|
+
isSymbolicLink: false,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
285
359
|
const entries = await nodeFs.readdir(this.toLocal(route.mount, route.relative), { withFileTypes: true });
|
|
286
360
|
return entries.map((e) => ({
|
|
287
361
|
name: e.name,
|
|
@@ -411,11 +485,17 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
411
485
|
const np = normalize(path);
|
|
412
486
|
const route = this.routeToMount(np);
|
|
413
487
|
if (route) {
|
|
488
|
+
if (route.mount.provider) {
|
|
489
|
+
// Provider mounts have no real disk path and don't support symlinks.
|
|
490
|
+
// The VFS path is its own canonical form.
|
|
491
|
+
return np;
|
|
492
|
+
}
|
|
414
493
|
// Mount contents on local disk: resolve via node, but report back
|
|
415
494
|
// in VFS-namespace terms (don't leak the on-disk source path to the
|
|
416
495
|
// agent — that would be confusing and non-portable).
|
|
417
496
|
const localResolved = await nodeFs.realpath(this.toLocal(route.mount, route.relative));
|
|
418
|
-
const
|
|
497
|
+
const source = route.mount.source!;
|
|
498
|
+
const localRoot = await nodeFs.realpath(source).catch(() => source);
|
|
419
499
|
if (localResolved === localRoot) return route.mount.prefixNoSlash;
|
|
420
500
|
if (localResolved.startsWith(localRoot + nodePath.sep)) {
|
|
421
501
|
const rel = localResolved.slice(localRoot.length + 1).split(nodePath.sep).join("/");
|
|
@@ -448,12 +528,16 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
448
528
|
for (const m of this.mounts) {
|
|
449
529
|
// Always advertise the mount root itself as a directory.
|
|
450
530
|
out.add(m.prefixNoSlash);
|
|
451
|
-
//
|
|
531
|
+
// Provider mounts: deep listing would need async IO; advertise the
|
|
532
|
+
// mount root only. Bash glob/find over the mount falls back to a
|
|
533
|
+
// shallow listing — acceptable per the v1 spec (corner case).
|
|
534
|
+
if (m.provider) continue;
|
|
535
|
+
// Disk-backed mount: walk the local source once and add all paths.
|
|
452
536
|
// Sync IO is acceptable here: bash glob/find call this sporadically and
|
|
453
537
|
// the source is a small static asset directory on the API container.
|
|
454
538
|
try {
|
|
455
539
|
const stack: Array<{ abs: string; vfs: string }> = [
|
|
456
|
-
{ abs: m.source
|
|
540
|
+
{ abs: m.source!, vfs: m.prefixNoSlash },
|
|
457
541
|
];
|
|
458
542
|
while (stack.length > 0) {
|
|
459
543
|
const { abs, vfs } = stack.pop()!;
|
|
@@ -514,6 +598,9 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
514
598
|
const np = normalize(path);
|
|
515
599
|
const route = this.routeToMount(np);
|
|
516
600
|
if (route) {
|
|
601
|
+
if (route.mount.provider) {
|
|
602
|
+
throw new Error(`EINVAL: not a symbolic link, readlink '${path}'`);
|
|
603
|
+
}
|
|
517
604
|
// Mount contents are real files; readlink only makes sense for symlinks
|
|
518
605
|
// we don't expect to have on disk. Node will throw EINVAL for non-links.
|
|
519
606
|
return nodeFs.readlink(this.toLocal(route.mount, route.relative));
|
|
@@ -525,6 +612,14 @@ export class PonchoFsAdapter implements IFileSystem {
|
|
|
525
612
|
const np = normalize(path);
|
|
526
613
|
const route = this.routeToMount(np);
|
|
527
614
|
if (route) {
|
|
615
|
+
if (route.mount.provider) {
|
|
616
|
+
// Provider mounts can't host symlinks; lstat == stat.
|
|
617
|
+
try {
|
|
618
|
+
return await route.mount.provider.stat(this.tenantId, route.relative);
|
|
619
|
+
} catch {
|
|
620
|
+
throw new Error(`ENOENT: no such file or directory, lstat '${path}'`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
528
623
|
try {
|
|
529
624
|
const s = await nodeFs.lstat(this.toLocal(route.mount, route.relative));
|
|
530
625
|
return this.toFsStat(s);
|