@poncho-ai/harness 0.47.1 → 0.49.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.47.1 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.49.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
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 527.60 KB
12
11
  ESM dist/isolate-VY35DGLM.js 49.43 KB
13
- ESM ⚡️ Build success in 275ms
12
+ ESM dist/index.js 528.58 KB
13
+ ESM ⚡️ Build success in 197ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 7513ms
16
- DTS dist/index.d.ts 86.25 KB
15
+ DTS ⚡️ Build success in 5685ms
16
+ DTS dist/index.d.ts 87.88 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.49.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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
8
+
9
+ Main memory injected into the system prompt was hard-truncated at 4000
10
+ characters with a `...[truncated]` marker. Silently dropping the tail of
11
+ a user's memory every turn is a footgun, so the **default is now no
12
+ truncation** — the full memory is injected.
13
+
14
+ New `MemoryConfig.maxPromptChars` (also settable via
15
+ `storage.memory.maxPromptChars`) lets a consumer opt back _into_ a cap
16
+ for prompt-cost control: set a positive number and content beyond it is
17
+ sliced with the `...[truncated]` marker as before.
18
+
19
+ Behavior change: consumers that relied on the implicit 4000-char cap
20
+ will now see full memory in the prompt. To restore the old behavior set
21
+ `maxPromptChars: 4000`.
22
+
23
+ ## 0.48.0
24
+
25
+ ### Minor Changes
26
+
27
+ - [#125](https://github.com/cesr/poncho-ai/pull/125) [`ff66aae`](https://github.com/cesr/poncho-ai/commit/ff66aaeebe6017ca9e1ee4b31ffe0d89bdf5ef28) Thanks [@cesr](https://github.com/cesr)! - harness: add `systemSkillPaths` for platform-shipped system skills
28
+
29
+ New optional `HarnessOptions.systemSkillPaths` (absolute directories,
30
+ each scanned for `<name>/SKILL.md` at init). System skills are surfaced
31
+ in `<available_skills>` like any other skill, with their bodies read
32
+ from local disk on activation — letting a platform ship default skills
33
+ with the deploy instead of writing them into every tenant's VFS.
34
+
35
+ Precedence is purely additive: per tenant the skill set resolves as
36
+ repo skills > the tenant's own VFS skills > system skills. So a tenant's
37
+ `/skills/<same-name>/` overrides a same-named system skill (mirroring
38
+ the VFS override behavior platforms already rely on for system jobs),
39
+ and the existing repo-vs-VFS precedence is unchanged. Empty by default —
40
+ no behavior change for existing consumers.
41
+
42
+ Also exports `loadSkillMetadataFromDirs(dirs)` (extracted from
43
+ `loadSkillMetadata`) for scanning an explicit list of absolute skill
44
+ directories.
45
+
3
46
  ## 0.47.1
4
47
 
5
48
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -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;
@@ -1211,6 +1221,17 @@ interface HarnessOptions {
1211
1221
  * Empty by default — no system mounts in the CLI / dev workflow.
1212
1222
  */
1213
1223
  virtualMounts?: VirtualMount[];
1224
+ /**
1225
+ * Absolute directories of platform-shipped "system" skills. Each is
1226
+ * scanned for `<name>/SKILL.md` at init; the bodies live on local disk
1227
+ * and ship with the deploy. System skills are surfaced in
1228
+ * `<available_skills>` like any other skill, but sit at the LOWEST
1229
+ * precedence: a tenant's own `/skills/<same-name>/` (and a repo skill)
1230
+ * overrides a system skill of the same name. Pair with a read-only
1231
+ * `virtualMounts` entry (e.g. "/system/skills/") if the same files
1232
+ * should also be browsable in the VFS. Empty by default.
1233
+ */
1234
+ systemSkillPaths?: string[];
1214
1235
  }
1215
1236
  interface HarnessRunOutput {
1216
1237
  runId: string;
@@ -1243,6 +1264,8 @@ declare class AgentHarness {
1243
1264
  private loadedConfig?;
1244
1265
  private readonly injectedConfig?;
1245
1266
  private loadedSkills;
1267
+ private systemSkills;
1268
+ private readonly systemSkillPaths;
1246
1269
  private skillFingerprint;
1247
1270
  private lastSkillRefreshAt;
1248
1271
  private readonly activeSkillNames;
@@ -1298,10 +1321,15 @@ declare class AgentHarness {
1298
1321
  private getMemoryStore;
1299
1322
  private listActiveSkills;
1300
1323
  /**
1301
- * Resolve the skill set visible to a given tenant: repo skills plus that
1302
- * tenant's VFS skills, with repo winning on name collision. Cached per
1303
- * tenant; cache invalidates on VFS writes under /skills/ via
1304
- * invalidateSkillsForTenant.
1324
+ * Resolve the skill set visible to a given tenant. Three tiers, by
1325
+ * precedence: repo skills > the tenant's own VFS skills > platform
1326
+ * system skills. So a repo skill wins over a same-named VFS skill
1327
+ * (unchanged), and a tenant's `/skills/<name>/` overrides a same-named
1328
+ * system skill (the deploy-shipped default). Cached per tenant; cache
1329
+ * invalidates on VFS writes under /skills/ via invalidateSkillsForTenant.
1330
+ * System skills are static within a process, so they don't participate
1331
+ * in the fingerprint — but a VFS override does (it changes a /skills
1332
+ * path), which recomputes the cache and lets the override take effect.
1305
1333
  */
1306
1334
  private getSkillsForTenant;
1307
1335
  invalidateSkillsForTenant(tenantId: string): void;
@@ -1451,6 +1479,7 @@ declare const parseSkillFrontmatter: (content: string) => {
1451
1479
  };
1452
1480
  } | undefined;
1453
1481
  declare const loadSkillMetadata: (workingDir: string, extraSkillPaths?: string[]) => Promise<SkillMetadata[]>;
1482
+ declare const loadSkillMetadataFromDirs: (skillDirs: string[]) => Promise<SkillMetadata[]>;
1454
1483
  declare const buildSkillContextWindow: (skills: SkillMetadata[]) => string;
1455
1484
  declare const loadVfsSkillMetadata: (engine: StorageEngine, tenantId: string) => Promise<SkillMetadata[]>;
1456
1485
  declare const mergeSkills: (repoSkills: SkillMetadata[], vfsSkills: SkillMetadata[], onCollision?: (vfsSkill: SkillMetadata) => void) => SkillMetadata[];
@@ -2080,4 +2109,4 @@ interface RunConversationTurnResult {
2080
2109
  }
2081
2110
  declare const runConversationTurn: (opts: RunConversationTurnOpts) => Promise<RunConversationTurnResult>;
2082
2111
 
2083
- 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, 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 };
2112
+ 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 };
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;
@@ -7363,7 +7364,9 @@ var collectSkillManifests = async (directory) => {
7363
7364
  return files;
7364
7365
  };
7365
7366
  var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
7366
- const skillDirs = resolveSkillDirs(workingDir, extraSkillPaths);
7367
+ return loadSkillMetadataFromDirs(resolveSkillDirs(workingDir, extraSkillPaths));
7368
+ };
7369
+ var loadSkillMetadataFromDirs = async (skillDirs) => {
7367
7370
  const allManifests = [];
7368
7371
  for (const dir of skillDirs) {
7369
7372
  try {
@@ -9120,6 +9123,8 @@ var AgentHarness = class _AgentHarness {
9120
9123
  loadedConfig;
9121
9124
  injectedConfig;
9122
9125
  loadedSkills = [];
9126
+ systemSkills = [];
9127
+ systemSkillPaths = [];
9123
9128
  skillFingerprint = "";
9124
9129
  lastSkillRefreshAt = 0;
9125
9130
  activeSkillNames = /* @__PURE__ */ new Set();
@@ -9325,6 +9330,7 @@ var AgentHarness = class _AgentHarness {
9325
9330
  this.injectedStorageEngine = true;
9326
9331
  }
9327
9332
  this.virtualMounts = options.virtualMounts ?? [];
9333
+ this.systemSkillPaths = options.systemSkillPaths ?? [];
9328
9334
  if (options.toolDefinitions?.length) {
9329
9335
  this.dispatcher.registerMany(options.toolDefinitions);
9330
9336
  }
@@ -9486,14 +9492,19 @@ var AgentHarness = class _AgentHarness {
9486
9492
  return [...this.activeSkillNames].sort();
9487
9493
  }
9488
9494
  /**
9489
- * Resolve the skill set visible to a given tenant: repo skills plus that
9490
- * tenant's VFS skills, with repo winning on name collision. Cached per
9491
- * tenant; cache invalidates on VFS writes under /skills/ via
9492
- * invalidateSkillsForTenant.
9495
+ * Resolve the skill set visible to a given tenant. Three tiers, by
9496
+ * precedence: repo skills > the tenant's own VFS skills > platform
9497
+ * system skills. So a repo skill wins over a same-named VFS skill
9498
+ * (unchanged), and a tenant's `/skills/<name>/` overrides a same-named
9499
+ * system skill (the deploy-shipped default). Cached per tenant; cache
9500
+ * invalidates on VFS writes under /skills/ via invalidateSkillsForTenant.
9501
+ * System skills are static within a process, so they don't participate
9502
+ * in the fingerprint — but a VFS override does (it changes a /skills
9503
+ * path), which recomputes the cache and lets the override take effect.
9493
9504
  */
9494
9505
  async getSkillsForTenant(tenantId) {
9495
9506
  if (!this.storageEngine) {
9496
- return this.loadedSkills;
9507
+ return mergeSkills(this.loadedSkills, this.systemSkills);
9497
9508
  }
9498
9509
  const effectiveTenant = tenantId || "__default__";
9499
9510
  const engineWithRefresh = this.storageEngine;
@@ -9506,7 +9517,7 @@ var AgentHarness = class _AgentHarness {
9506
9517
  return cached.skills;
9507
9518
  }
9508
9519
  const vfsSkills = await loadVfsSkillMetadata(this.storageEngine, effectiveTenant);
9509
- const merged = mergeSkills(this.loadedSkills, vfsSkills, (skipped) => {
9520
+ const repoAndVfs = mergeSkills(this.loadedSkills, vfsSkills, (skipped) => {
9510
9521
  const key = `${effectiveTenant}:${skipped.name}`;
9511
9522
  if (this.vfsSkillCollisionWarnings.has(key)) return;
9512
9523
  this.vfsSkillCollisionWarnings.add(key);
@@ -9514,6 +9525,7 @@ var AgentHarness = class _AgentHarness {
9514
9525
  `VFS skill "${skipped.name}" for tenant ${effectiveTenant} ignored: a repo skill with the same name takes precedence.`
9515
9526
  );
9516
9527
  });
9528
+ const merged = mergeSkills(repoAndVfs, this.systemSkills);
9517
9529
  this.skillCache.set(effectiveTenant, { skills: merged, fingerprint });
9518
9530
  return merged;
9519
9531
  }
@@ -9844,6 +9856,7 @@ var AgentHarness = class _AgentHarness {
9844
9856
  const extraSkillPaths = config?.skillPaths;
9845
9857
  const skillMetadata = await loadSkillMetadata(this.workingDir, extraSkillPaths);
9846
9858
  this.loadedSkills = skillMetadata;
9859
+ this.systemSkills = this.systemSkillPaths.length ? await loadSkillMetadataFromDirs(this.systemSkillPaths) : [];
9847
9860
  this.skillFingerprint = this.buildSkillFingerprint(skillMetadata);
9848
9861
  this.registerSkillTools();
9849
9862
  const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
@@ -10221,7 +10234,8 @@ Browser sessions (cookies, localStorage, login state) are automatically saved an
10221
10234
  ### Tabs and resources
10222
10235
  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.` : "";
10223
10236
  const mainMemory = await memoryPromise;
10224
- const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
10237
+ const memCap = this.memoryConfig?.maxPromptChars ?? 0;
10238
+ const boundedMainMemory = mainMemory && memCap > 0 && mainMemory.content.length > memCap ? `${mainMemory.content.slice(0, memCap)}
10225
10239
  ...[truncated]` : mainMemory?.content;
10226
10240
  const memoryContext = boundedMainMemory && boundedMainMemory.trim().length > 0 ? `
10227
10241
  ## Persistent Memory
@@ -13980,6 +13994,7 @@ export {
13980
13994
  loadSkillContext,
13981
13995
  loadSkillInstructions,
13982
13996
  loadSkillMetadata,
13997
+ loadSkillMetadataFromDirs,
13983
13998
  loadVfsSkillMetadata,
13984
13999
  mergeSkills,
13985
14000
  normalizeApprovalCheckpoint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.47.1",
3
+ "version": "0.49.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
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
@@ -55,6 +55,7 @@ import { createModelProvider, getModelContextWindow, type ModelProviderFactory,
55
55
  import {
56
56
  buildSkillContextWindow,
57
57
  loadSkillMetadata,
58
+ loadSkillMetadataFromDirs,
58
59
  loadVfsSkillMetadata,
59
60
  mergeSkills,
60
61
  } from "./skill-context.js";
@@ -134,6 +135,17 @@ export interface HarnessOptions {
134
135
  * Empty by default — no system mounts in the CLI / dev workflow.
135
136
  */
136
137
  virtualMounts?: VirtualMount[];
138
+ /**
139
+ * Absolute directories of platform-shipped "system" skills. Each is
140
+ * scanned for `<name>/SKILL.md` at init; the bodies live on local disk
141
+ * and ship with the deploy. System skills are surfaced in
142
+ * `<available_skills>` like any other skill, but sit at the LOWEST
143
+ * precedence: a tenant's own `/skills/<same-name>/` (and a repo skill)
144
+ * overrides a system skill of the same name. Pair with a read-only
145
+ * `virtualMounts` entry (e.g. "/system/skills/") if the same files
146
+ * should also be browsable in the VFS. Empty by default.
147
+ */
148
+ systemSkillPaths?: string[];
137
149
  }
138
150
 
139
151
  export interface HarnessRunOutput {
@@ -839,6 +851,8 @@ export class AgentHarness {
839
851
  private loadedConfig?: PonchoConfig;
840
852
  private readonly injectedConfig?: PonchoConfig;
841
853
  private loadedSkills: SkillMetadata[] = [];
854
+ private systemSkills: SkillMetadata[] = [];
855
+ private readonly systemSkillPaths: string[] = [];
842
856
  private skillFingerprint = "";
843
857
  private lastSkillRefreshAt = 0;
844
858
  private readonly activeSkillNames = new Set<string>();
@@ -1077,6 +1091,7 @@ export class AgentHarness {
1077
1091
  this.injectedStorageEngine = true;
1078
1092
  }
1079
1093
  this.virtualMounts = options.virtualMounts ?? [];
1094
+ this.systemSkillPaths = options.systemSkillPaths ?? [];
1080
1095
 
1081
1096
  if (options.toolDefinitions?.length) {
1082
1097
  this.dispatcher.registerMany(options.toolDefinitions);
@@ -1271,14 +1286,19 @@ export class AgentHarness {
1271
1286
  }
1272
1287
 
1273
1288
  /**
1274
- * Resolve the skill set visible to a given tenant: repo skills plus that
1275
- * tenant's VFS skills, with repo winning on name collision. Cached per
1276
- * tenant; cache invalidates on VFS writes under /skills/ via
1277
- * invalidateSkillsForTenant.
1289
+ * Resolve the skill set visible to a given tenant. Three tiers, by
1290
+ * precedence: repo skills > the tenant's own VFS skills > platform
1291
+ * system skills. So a repo skill wins over a same-named VFS skill
1292
+ * (unchanged), and a tenant's `/skills/<name>/` overrides a same-named
1293
+ * system skill (the deploy-shipped default). Cached per tenant; cache
1294
+ * invalidates on VFS writes under /skills/ via invalidateSkillsForTenant.
1295
+ * System skills are static within a process, so they don't participate
1296
+ * in the fingerprint — but a VFS override does (it changes a /skills
1297
+ * path), which recomputes the cache and lets the override take effect.
1278
1298
  */
1279
1299
  private async getSkillsForTenant(tenantId: string | undefined | null): Promise<SkillMetadata[]> {
1280
1300
  if (!this.storageEngine) {
1281
- return this.loadedSkills;
1301
+ return mergeSkills(this.loadedSkills, this.systemSkills);
1282
1302
  }
1283
1303
  // Mirror the rest of the harness: undefined tenantId falls back to
1284
1304
  // "__default__" so dev-mode (no auth) conversations see the same VFS
@@ -1305,7 +1325,7 @@ export class AgentHarness {
1305
1325
  return cached.skills;
1306
1326
  }
1307
1327
  const vfsSkills = await loadVfsSkillMetadata(this.storageEngine, effectiveTenant);
1308
- const merged = mergeSkills(this.loadedSkills, vfsSkills, (skipped) => {
1328
+ const repoAndVfs = mergeSkills(this.loadedSkills, vfsSkills, (skipped) => {
1309
1329
  const key = `${effectiveTenant}:${skipped.name}`;
1310
1330
  if (this.vfsSkillCollisionWarnings.has(key)) return;
1311
1331
  this.vfsSkillCollisionWarnings.add(key);
@@ -1313,6 +1333,11 @@ export class AgentHarness {
1313
1333
  `VFS skill "${skipped.name}" for tenant ${effectiveTenant} ignored: a repo skill with the same name takes precedence.`,
1314
1334
  );
1315
1335
  });
1336
+ // System skills sit at the bottom: a repo or VFS skill of the same
1337
+ // name overrides them. Overriding a system default is the intended
1338
+ // user workflow (mirrors /jobs system-default overrides), so the
1339
+ // collision is silent — not a warning.
1340
+ const merged = mergeSkills(repoAndVfs, this.systemSkills);
1316
1341
  this.skillCache.set(effectiveTenant, { skills: merged, fingerprint });
1317
1342
  return merged;
1318
1343
  }
@@ -1706,6 +1731,13 @@ export class AgentHarness {
1706
1731
  const extraSkillPaths = config?.skillPaths;
1707
1732
  const skillMetadata = await loadSkillMetadata(this.workingDir, extraSkillPaths);
1708
1733
  this.loadedSkills = skillMetadata;
1734
+ // Platform-shipped system skills, scanned from absolute dirs on disk.
1735
+ // Loaded once at init (they ship with the deploy and don't change
1736
+ // within a process). Merged at LOWEST precedence per tenant — see
1737
+ // getSkillsForTenant.
1738
+ this.systemSkills = this.systemSkillPaths.length
1739
+ ? await loadSkillMetadataFromDirs(this.systemSkillPaths)
1740
+ : [];
1709
1741
  this.skillFingerprint = this.buildSkillFingerprint(skillMetadata);
1710
1742
  this.registerSkillTools();
1711
1743
  const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
@@ -2152,9 +2184,15 @@ Browser sessions (cookies, localStorage, login state) are automatically saved an
2152
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.`
2153
2185
  : "";
2154
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;
2155
2193
  const boundedMainMemory =
2156
- mainMemory && mainMemory.content.length > 4000
2157
- ? `${mainMemory.content.slice(0, 4000)}\n...[truncated]`
2194
+ mainMemory && memCap > 0 && mainMemory.content.length > memCap
2195
+ ? `${mainMemory.content.slice(0, memCap)}\n...[truncated]`
2158
2196
  : mainMemory?.content;
2159
2197
  const memoryContext =
2160
2198
  boundedMainMemory && boundedMainMemory.trim().length > 0
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 {
@@ -209,7 +209,17 @@ export const loadSkillMetadata = async (
209
209
  workingDir: string,
210
210
  extraSkillPaths?: string[],
211
211
  ): Promise<SkillMetadata[]> => {
212
- const skillDirs = resolveSkillDirs(workingDir, extraSkillPaths);
212
+ return loadSkillMetadataFromDirs(resolveSkillDirs(workingDir, extraSkillPaths));
213
+ };
214
+
215
+ // Scan an explicit list of absolute directories for `<name>/SKILL.md`
216
+ // manifests and return their metadata as `source: "repo"` skills (body
217
+ // read from disk on activation). Used both by `loadSkillMetadata` (after
218
+ // resolving repo skill dirs against the working dir) and directly for
219
+ // platform-shipped "system" skills whose source dirs are already absolute.
220
+ export const loadSkillMetadataFromDirs = async (
221
+ skillDirs: string[],
222
+ ): Promise<SkillMetadata[]> => {
213
223
  const allManifests: string[] = [];
214
224
 
215
225
  for (const dir of skillDirs) {