@tpsdev-ai/agent 0.1.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +54 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +32 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/config.d.ts +23 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +39 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/governance/boundary.d.ts +30 -0
  11. package/dist/governance/boundary.d.ts.map +1 -0
  12. package/dist/governance/boundary.js +120 -0
  13. package/dist/governance/boundary.js.map +1 -0
  14. package/dist/governance/review-gate.d.ts +9 -0
  15. package/dist/governance/review-gate.d.ts.map +1 -0
  16. package/dist/governance/review-gate.js +28 -0
  17. package/dist/governance/review-gate.js.map +1 -0
  18. package/dist/index.d.ts +16 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +19 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/io/context.d.ts +18 -0
  23. package/dist/io/context.d.ts.map +1 -0
  24. package/dist/io/context.js +76 -0
  25. package/dist/io/context.js.map +1 -0
  26. package/dist/io/mail.d.ts +22 -0
  27. package/dist/io/mail.d.ts.map +1 -0
  28. package/dist/io/mail.js +45 -0
  29. package/dist/io/mail.js.map +1 -0
  30. package/dist/io/memory.d.ts +24 -0
  31. package/dist/io/memory.d.ts.map +1 -0
  32. package/dist/io/memory.js +91 -0
  33. package/dist/io/memory.js.map +1 -0
  34. package/dist/llm/provider.d.ts +26 -0
  35. package/dist/llm/provider.d.ts.map +1 -0
  36. package/dist/llm/provider.js +254 -0
  37. package/dist/llm/provider.js.map +1 -0
  38. package/dist/runtime/agent.d.ts +14 -0
  39. package/dist/runtime/agent.d.ts.map +1 -0
  40. package/dist/runtime/agent.js +46 -0
  41. package/dist/runtime/agent.js.map +1 -0
  42. package/dist/runtime/event-loop.d.ts +32 -0
  43. package/dist/runtime/event-loop.d.ts.map +1 -0
  44. package/dist/runtime/event-loop.js +178 -0
  45. package/dist/runtime/event-loop.js.map +1 -0
  46. package/dist/runtime/types.d.ts +66 -0
  47. package/dist/runtime/types.d.ts.map +1 -0
  48. package/dist/runtime/types.js +2 -0
  49. package/dist/runtime/types.js.map +1 -0
  50. package/dist/tools/edit.d.ts +4 -0
  51. package/dist/tools/edit.d.ts.map +1 -0
  52. package/dist/tools/edit.js +46 -0
  53. package/dist/tools/edit.js.map +1 -0
  54. package/dist/tools/exec.d.ts +4 -0
  55. package/dist/tools/exec.d.ts.map +1 -0
  56. package/dist/tools/exec.js +74 -0
  57. package/dist/tools/exec.js.map +1 -0
  58. package/dist/tools/index.d.ts +17 -0
  59. package/dist/tools/index.d.ts.map +1 -0
  60. package/dist/tools/index.js +23 -0
  61. package/dist/tools/index.js.map +1 -0
  62. package/dist/tools/mail.d.ts +4 -0
  63. package/dist/tools/mail.d.ts.map +1 -0
  64. package/dist/tools/mail.js +19 -0
  65. package/dist/tools/mail.js.map +1 -0
  66. package/dist/tools/read.d.ts +4 -0
  67. package/dist/tools/read.d.ts.map +1 -0
  68. package/dist/tools/read.js +31 -0
  69. package/dist/tools/read.js.map +1 -0
  70. package/dist/tools/registry.d.ts +24 -0
  71. package/dist/tools/registry.d.ts.map +1 -0
  72. package/dist/tools/registry.js +23 -0
  73. package/dist/tools/registry.js.map +1 -0
  74. package/dist/tools/write.d.ts +4 -0
  75. package/dist/tools/write.d.ts.map +1 -0
  76. package/dist/tools/write.js +32 -0
  77. package/dist/tools/write.js.map +1 -0
  78. package/package.json +19 -4
  79. package/src/bin.ts +40 -0
  80. package/src/config.ts +64 -0
  81. package/src/governance/boundary.ts +112 -18
  82. package/src/index.ts +15 -2
  83. package/src/io/context.ts +59 -15
  84. package/src/io/memory.ts +53 -9
  85. package/src/llm/provider.ts +203 -42
  86. package/src/runtime/agent.ts +30 -3
  87. package/src/runtime/event-loop.ts +168 -26
  88. package/src/runtime/types.ts +54 -6
  89. package/src/tools/edit.ts +59 -0
  90. package/src/tools/exec.ts +92 -0
  91. package/src/tools/index.ts +30 -0
  92. package/src/tools/mail.ts +28 -0
  93. package/src/tools/read.ts +38 -0
  94. package/src/tools/registry.ts +16 -7
  95. package/src/tools/write.ts +40 -0
  96. package/src/types/js-tiktoken.d.ts +7 -0
  97. package/test/governance.test.ts +61 -32
  98. package/test/io.test.ts +15 -7
  99. package/test/runtime.test.ts +26 -5
@@ -0,0 +1,32 @@
1
+ import { mkdirSync, writeFileSync, existsSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { BoundaryManager } from "../governance/boundary.js";
4
+ export function makeWriteTool(boundary) {
5
+ return {
6
+ name: "write",
7
+ description: "Create or overwrite a file.\nInput: {\"path\": string, \"content\": string}",
8
+ input_schema: {
9
+ path: { type: "string", description: "Path to write, relative to workspace" },
10
+ content: { type: "string", description: "File contents" },
11
+ },
12
+ async execute(args) {
13
+ const payload = args;
14
+ if (typeof payload.path !== "string" || typeof payload.content !== "string") {
15
+ return { content: "write tool requires path and content strings", isError: true };
16
+ }
17
+ const workspacePath = boundary.resolveWorkspacePath(payload.path);
18
+ if (existsSync(workspacePath) && !BoundaryManager.canFollowSymlink(workspacePath)) {
19
+ return { content: `Refusing to follow symlink: ${payload.path}`, isError: true };
20
+ }
21
+ try {
22
+ mkdirSync(dirname(workspacePath), { recursive: true });
23
+ writeFileSync(workspacePath, payload.content, "utf-8");
24
+ return { content: `Wrote ${payload.path}`, isError: false };
25
+ }
26
+ catch (err) {
27
+ return { content: `write failed: ${err?.message ?? String(err)}`, isError: true };
28
+ }
29
+ },
30
+ };
31
+ }
32
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAS5D,MAAM,UAAU,aAAa,CAAC,QAAyB;IACrD,OAAO;QACL,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,6EAA6E;QAC1F,YAAY,EAAE;YACZ,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sCAAsC,EAAE;YAC7E,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE;SAC1D;QACD,KAAK,CAAC,OAAO,CAAC,IAA6B;YACzC,MAAM,OAAO,GAAI,IAA6B,CAAC;YAC/C,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC5E,OAAO,EAAE,OAAO,EAAE,8CAA8C,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACpF,CAAC;YAED,MAAM,aAAa,GAAG,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAClE,IAAI,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,aAAa,CAAC,EAAE,CAAC;gBAClF,OAAO,EAAE,OAAO,EAAE,+BAA+B,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACnF,CAAC;YAED,IAAI,CAAC;gBACH,SAAS,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACvD,aAAa,CAAC,aAAa,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBACvD,OAAO,EAAE,OAAO,EAAE,SAAS,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC9D,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,OAAO,EAAE,OAAO,EAAE,iBAAiB,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACpF,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/agent",
3
- "version": "0.1.0",
4
- "description": "Native TPS Agent Runtime headless, mail-driven, nono-sandboxed",
3
+ "version": "0.4.1",
4
+ "description": "Native TPS Agent Runtime \u2014 headless, mail-driven, nono-sandboxed",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -9,8 +9,15 @@
9
9
  ".": {
10
10
  "import": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
+ },
13
+ "./bin": {
14
+ "import": "./dist/bin.js",
15
+ "types": "./dist/bin.d.ts"
12
16
  }
13
17
  },
18
+ "bin": {
19
+ "tps-agent": "./dist/bin.js"
20
+ },
14
21
  "scripts": {
15
22
  "build": "tsc",
16
23
  "dev": "tsc --watch",
@@ -18,10 +25,16 @@
18
25
  "lint": "biome lint ./src",
19
26
  "lint:ci": "biome lint ./src --max-diagnostics=200"
20
27
  },
21
- "keywords": ["agents", "tps", "runtime", "mail-driven"],
28
+ "keywords": [
29
+ "agents",
30
+ "tps",
31
+ "runtime",
32
+ "mail-driven"
33
+ ],
22
34
  "license": "Apache-2.0",
23
35
  "dependencies": {
24
36
  "js-yaml": "^4.1.0",
37
+ "js-tiktoken": "^1.0.13",
25
38
  "zod": "^3.24.0"
26
39
  },
27
40
  "devDependencies": {
@@ -31,5 +44,7 @@
31
44
  "typescript": "^5.7.0"
32
45
  },
33
46
  "author": "tpsdev-ai",
34
- "publishConfig": { "access": "public" }
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
35
50
  }
package/src/bin.ts ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { AgentRuntime } from "./runtime/agent.js";
3
+ import { loadAgentConfig } from "./config.js";
4
+
5
+ function usage(): never {
6
+ console.error("Usage: tps-agent run --config <agent.yaml> --message <text>");
7
+ process.exit(1);
8
+ }
9
+
10
+ async function main() {
11
+ const args = process.argv.slice(2);
12
+ const cmd = args[0];
13
+
14
+ if (cmd !== "run") {
15
+ usage();
16
+ }
17
+
18
+ const configIdx = args.indexOf("--config");
19
+ const msgIdx = args.indexOf("--message");
20
+
21
+ if (configIdx < 0 || msgIdx < 0) {
22
+ usage();
23
+ }
24
+
25
+ const configPath = args[configIdx + 1];
26
+ const message = args.slice(msgIdx + 1).join(" ");
27
+
28
+ if (!configPath || !message) {
29
+ usage();
30
+ }
31
+
32
+ const config = loadAgentConfig(configPath);
33
+ const runtime = new AgentRuntime(config);
34
+ await runtime.runOnce(message);
35
+ }
36
+
37
+ main().catch((err) => {
38
+ console.error(err?.message || err);
39
+ process.exit(1);
40
+ });
package/src/config.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from "node:fs";
2
+ import yaml from "js-yaml";
3
+ import { createHash } from "node:crypto";
4
+ import type { AgentConfig } from "./runtime/types.js";
5
+
6
+ export interface AgentRuntimeConfig {
7
+ agentId: string;
8
+ name: string;
9
+ mailDir: string;
10
+ memoryPath?: string;
11
+ workspace: string;
12
+ systemPrompt?: string;
13
+ llm: {
14
+ provider: "anthropic" | "google" | "openai" | "ollama";
15
+ model: string;
16
+ apiKey?: string;
17
+ baseUrl?: string;
18
+ };
19
+ contextWindowTokens?: number;
20
+ maxTokens?: number;
21
+ tools?: Array<"read" | "write" | "edit" | "exec" | "mail">;
22
+ execAllowlist?: string[];
23
+ }
24
+
25
+ export function loadAgentConfig(path: string): AgentConfig {
26
+ const raw = readFileSync(path, "utf-8");
27
+ const parsed = (yaml.load(raw) ?? {}) as Record<string, any>;
28
+
29
+ const workspace = String(parsed.workspace || parsed.repo || process.cwd());
30
+ const memoryPath = parsed.memoryPath || `${workspace}/.openclaw/agent.memory.jsonl`;
31
+
32
+ const agent: AgentConfig = {
33
+ agentId: String(parsed.agentId || parsed.id || "agent"),
34
+ name: String(parsed.name || parsed.agentId || "agent"),
35
+ workspace,
36
+ mailDir: String(parsed.mailDir || `${workspace}/mail`),
37
+ memoryPath: String(memoryPath),
38
+ systemPrompt: parsed.systemPrompt ? String(parsed.systemPrompt) : undefined,
39
+ contextWindowTokens: parsed.contextWindowTokens ? Number(parsed.contextWindowTokens) : 8192,
40
+ maxTokens: parsed.maxTokens ? Number(parsed.maxTokens) : 1024,
41
+ tools: parsed.tools ?? ["read", "write", "edit", "exec", "mail"],
42
+ execAllowlist: parsed.execAllowlist,
43
+ llm: {
44
+ provider: parsed.llm?.provider || parsed.provider || "openai",
45
+ model: parsed.llm?.model || parsed.model || "gpt-4o-mini",
46
+ apiKey: parsed.llm?.apiKey || parsed.apiKey,
47
+ baseUrl: parsed.llm?.baseUrl || parsed.baseUrl,
48
+ },
49
+ };
50
+
51
+ return agent;
52
+ }
53
+
54
+ export function defaultMemoryPath(workspace: string): string {
55
+ const digest = createHash("sha1").update(workspace).digest("hex").slice(0, 8);
56
+ return `${workspace}/.openclaw/memory-${digest}.jsonl`;
57
+ }
58
+
59
+ export function normalizeConfigObject(value: unknown): AgentRuntimeConfig {
60
+ if (typeof value !== "object" || !value) {
61
+ throw new Error("Invalid agent config");
62
+ }
63
+ return value as AgentRuntimeConfig;
64
+ }
@@ -1,37 +1,131 @@
1
+ import { resolve, relative, isAbsolute, sep } from "node:path";
2
+ import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
3
+
1
4
  /**
2
- * BoundaryManager discovers nono profile constraints at boot and communicates
3
- * allowed capabilities to the ToolRegistry.
4
- *
5
- * NOTE: The system prompt is NOT a security boundary. Hard nono syscall/network
6
- * filters and pre-execution checks in ToolRegistry are the actual enforcement.
5
+ * BoundaryManager handles execution-time policy controls for security boundaries.
7
6
  */
8
7
  export class BoundaryManager {
9
- private allowedNetworkHosts: Set<string> = new Set();
10
- private allowedPaths: Set<string> = new Set();
8
+ private allowedNetworkHosts = new Set<string>();
9
+
10
+ constructor(private readonly workspace: string) {}
11
11
 
12
12
  addNetworkHost(host: string): void {
13
13
  this.allowedNetworkHosts.add(host);
14
14
  }
15
15
 
16
- addPath(path: string): void {
17
- this.allowedPaths.add(path);
18
- }
19
-
20
16
  isNetworkAllowed(host: string): boolean {
21
17
  return this.allowedNetworkHosts.has(host) || this.allowedNetworkHosts.has("*");
22
18
  }
23
19
 
24
- isPathAllowed(path: string): boolean {
25
- for (const allowed of this.allowedPaths) {
26
- if (path === allowed || path.startsWith(allowed + "/")) return true;
20
+ /**
21
+ * Resolve a user requested filesystem path against workspace and ensure
22
+ * it cannot escape the workspace root.
23
+ */
24
+ resolveWorkspacePath(relativeOrAbsolutePath: string): string {
25
+ const target = resolve(this.workspace, relativeOrAbsolutePath);
26
+ const workspaceReal = realpathSync(this.workspace);
27
+
28
+ let normalized = target;
29
+ if (existsSync(target)) {
30
+ normalized = realpathSync(target);
31
+ } else {
32
+ // If target does not exist, resolve relative to existing parent.
33
+ const parent = resolve(target, "..");
34
+ if (existsSync(parent)) {
35
+ normalized = resolve(realpathSync(parent), target.slice(parent.length + 1));
36
+ }
37
+ }
38
+
39
+ if (!this.isWithinWorkspace(normalized, workspaceReal)) {
40
+ throw new Error(`Path traversal blocked: ${relativeOrAbsolutePath}`);
41
+ }
42
+
43
+ return target;
44
+ }
45
+
46
+ private isWithinWorkspace(candidate: string, workspaceReal: string): boolean {
47
+ const rel = relative(workspaceReal, candidate);
48
+ if (rel === "") return true;
49
+ if (rel === ".." || rel.startsWith(`..${sep}`)) return false;
50
+ return !isAbsolute(rel);
51
+ }
52
+
53
+ /**
54
+ * Verify that a command is safe before exec.
55
+ */
56
+ validateCommand(command: string, args: string[]): void {
57
+ if (!command) {
58
+ throw new Error("Exec requires a command");
59
+ }
60
+
61
+ const blockedFlags = ["--exec-path", "-e", "--eval", "-p", "-c", "/dev/fd", "--noprofile", "--norc", "node_options", "command="];
62
+ const tokens = [command, ...args].map((token) => String(token).toLowerCase());
63
+
64
+ for (const token of tokens) {
65
+ if (blockedFlags.includes(token) || token.includes("core.pager")) {
66
+ throw new Error(`Disallowed exec argument: ${token}`);
67
+ }
68
+ }
69
+
70
+ // Disallow attempts to execute compound expressions.
71
+ const full = tokens.join(" ");
72
+ if (full.includes("||") || full.includes("&&") || full.includes(";") || full.includes("|") || full.includes("$") || full.includes("`") ) {
73
+ throw new Error(`Disallowed exec argument: ${full}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Child processes must not inherit runtime secrets from the parent process.
79
+ */
80
+ scrubEnvironment(extraKeys: string[] = []): NodeJS.ProcessEnv {
81
+ const env = { ...process.env } as NodeJS.ProcessEnv;
82
+ const denyPattern = /(API_KEY|APISECRET|SECRET|TOKEN|PASS|CREDENTIAL|AUTH)/i;
83
+
84
+ for (const key of Object.keys(env)) {
85
+ if (denyPattern.test(key)) {
86
+ delete env[key];
87
+ }
27
88
  }
28
- return false;
89
+
90
+ for (const key of extraKeys) {
91
+ if (key in env) {
92
+ delete env[key];
93
+ }
94
+ }
95
+
96
+ return env;
29
97
  }
30
98
 
31
- /** Returns a human-readable capabilities summary for the LLM system prompt. */
99
+ /** Returns human-readable boundaries for system prompts and audit logs. */
32
100
  describeCapabilities(): string {
33
101
  const nets = [...this.allowedNetworkHosts].join(", ") || "none";
34
- const paths = [...this.allowedPaths].join(", ") || "none";
35
- return `Network access: ${nets}\nFilesystem access: ${paths}`;
102
+ return `Network access: ${nets}\nFilesystem access: ${this.workspace}`;
103
+ }
104
+
105
+ static isFileReadable(path: string): boolean {
106
+ try {
107
+ accessSync(path, constants.R_OK);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ static isFileWritable(path: string): boolean {
115
+ try {
116
+ accessSync(path, constants.W_OK);
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ static canFollowSymlink(path: string): boolean {
124
+ try {
125
+ const stat = lstatSync(path);
126
+ return !stat.isSymbolicLink();
127
+ } catch {
128
+ return false;
129
+ }
36
130
  }
37
131
  }
package/src/index.ts CHANGED
@@ -3,7 +3,17 @@
3
3
  // Runtime
4
4
  export { AgentRuntime } from "./runtime/agent.js";
5
5
  export { EventLoop } from "./runtime/event-loop.js";
6
- export type { AgentConfig, LLMConfig, AgentState } from "./runtime/types.js";
6
+ export type {
7
+ AgentConfig,
8
+ LLMConfig,
9
+ AgentState,
10
+ CompletionRequest,
11
+ CompletionResponse,
12
+ ToolCall,
13
+ ToolSpec,
14
+ LLMMessage,
15
+ ToolResult,
16
+ } from "./runtime/types.js";
7
17
 
8
18
  // I/O
9
19
  export { MailClient } from "./io/mail.js";
@@ -14,12 +24,15 @@ export { ContextManager } from "./io/context.js";
14
24
 
15
25
  // LLM
16
26
  export { ProviderManager } from "./llm/provider.js";
17
- export type { CompletionRequest, CompletionResponse } from "./llm/provider.js";
18
27
 
19
28
  // Tools
20
29
  export { ToolRegistry } from "./tools/registry.js";
21
30
  export type { Tool } from "./tools/registry.js";
31
+ export { createDefaultToolset } from "./tools/index.js";
22
32
 
23
33
  // Governance
24
34
  export { BoundaryManager } from "./governance/boundary.js";
25
35
  export { ReviewGate } from "./governance/review-gate.js";
36
+
37
+ // Config
38
+ export { loadAgentConfig } from "./config.js";
package/src/io/context.ts CHANGED
@@ -1,40 +1,84 @@
1
1
  import type { MemoryStore, MemoryEvent } from "./memory.js";
2
2
 
3
3
  /**
4
- * Sliding window context manager with compaction.
5
- *
6
- * When token usage approaches the configured window, the oldest 50%
7
- * of events are summarised into a dense state blob to preserve headroom
8
- * without losing history.
4
+ * Sliding window context manager with token-based compaction.
9
5
  */
10
6
  export class ContextManager {
7
+ private encoder?: (text: string) => Promise<number[]> | number[];
8
+
11
9
  constructor(
12
10
  private readonly memory: MemoryStore,
13
11
  private readonly windowTokens: number
14
- ) {}
12
+ ) {
13
+ this.encoder = undefined;
14
+ }
15
+
16
+ private async getEncoder(): Promise<(text: string) => Promise<number[]> | number[]> {
17
+ if (this.encoder) return this.encoder;
18
+
19
+ try {
20
+ const mod = await import("js-tiktoken");
21
+ const fn =
22
+ (mod as any).encodingForModel?.("gpt-4o") ||
23
+ (mod as any).encoding_for_model?.("gpt-4o") ||
24
+ (mod as any).getEncoding?.("cl100k_base");
25
+
26
+ if (fn) {
27
+ this.encoder = (text: string) => {
28
+ const tokens = fn.encode(text);
29
+ if (Array.isArray(tokens)) return tokens;
30
+ return [];
31
+ };
32
+ return this.encoder;
33
+ }
34
+ } catch {}
35
+
36
+ // Fallback: approximate tokenization (4 chars ~= 1 token)
37
+ this.encoder = (text: string) => {
38
+ const count = Math.max(1, Math.ceil(text.length / 4));
39
+ return new Array(count);
40
+ };
41
+ return this.encoder;
42
+ }
43
+
44
+ private async countTokens(text: string): Promise<number> {
45
+ const encode = await this.getEncoder();
46
+ const tokens = await encode(text);
47
+ return Array.isArray(tokens) ? tokens.length : 0;
48
+ }
15
49
 
16
- /** Return recent events that fit within the token window. */
17
- getWindow(): MemoryEvent[] {
50
+ /** Return recent events that fit within token window. */
51
+ async getWindow(): Promise<MemoryEvent[]> {
18
52
  const all = this.memory.readAll();
19
53
  if (all.length === 0) return [];
20
54
 
21
- // Simple sliding-window: walk from newest, accumulate until budget
22
- const budget = this.windowTokens * 4; // chars ≈ tokens * 4
23
- let budget_remaining = budget;
55
+ const budget = this.windowTokens;
56
+ let budgetRemaining = budget;
24
57
  const window: MemoryEvent[] = [];
25
58
 
26
59
  for (let i = all.length - 1; i >= 0; i--) {
27
60
  const serialized = JSON.stringify(all[i]);
28
- if (serialized.length > budget_remaining) break;
61
+ const tokenCount = await this.countTokens(serialized);
62
+ if (tokenCount > budgetRemaining) break;
29
63
  window.unshift(all[i]!);
30
- budget_remaining -= serialized.length;
64
+ budgetRemaining -= tokenCount;
31
65
  }
32
66
 
33
67
  return window;
34
68
  }
35
69
 
36
70
  /** Returns true if compaction is needed (>80% of window used). */
37
- needsCompaction(): boolean {
38
- return this.memory.estimatedTokenCount() > this.windowTokens * 0.8;
71
+ async needsCompaction(): Promise<boolean> {
72
+ const total = await this.estimateTokenCount();
73
+ return total > this.windowTokens * 0.8;
74
+ }
75
+
76
+ async estimateTokenCount(): Promise<number> {
77
+ const events = this.memory.readAll();
78
+ let total = 0;
79
+ for (const event of events) {
80
+ total += await this.countTokens(JSON.stringify(event));
81
+ }
82
+ return total;
39
83
  }
40
84
  }
package/src/io/memory.ts CHANGED
@@ -9,7 +9,7 @@ export interface MemoryEvent {
9
9
 
10
10
  /**
11
11
  * Append-only JSONL memory store. Each event is a line of JSON.
12
- * Provides full audit trail of agent actions and LLM interactions.
12
+ * Provides a full audit trail of agent actions and LLM interactions.
13
13
  */
14
14
  export class MemoryStore {
15
15
  constructor(public readonly memoryPath: string) {
@@ -17,27 +17,28 @@ export class MemoryStore {
17
17
  }
18
18
 
19
19
  append(event: MemoryEvent): void {
20
- appendFileSync(this.memoryPath, JSON.stringify(event) + "\n", "utf-8");
20
+ const payload = this.sanitize(event);
21
+ appendFileSync(this.memoryPath, JSON.stringify(payload) + "\n", "utf-8");
21
22
  }
22
23
 
23
24
  readAll(maxBytes = 1024 * 1024): MemoryEvent[] {
24
25
  if (!existsSync(this.memoryPath)) return [];
25
-
26
+
26
27
  const stat = statSync(this.memoryPath);
27
28
  if (stat.size === 0) return [];
28
-
29
+
29
30
  const size = Math.min(stat.size, maxBytes);
30
31
  const pos = Math.max(0, stat.size - size);
31
-
32
+
32
33
  const fd = openSync(this.memoryPath, "r");
33
34
  const buffer = Buffer.alloc(size);
34
35
  readSync(fd, buffer, 0, size, pos);
35
36
  closeSync(fd);
36
-
37
+
37
38
  const content = buffer.toString("utf-8");
38
39
  const lines = content.split("\n");
39
- if (pos > 0) lines.shift(); // drop partial first line
40
-
40
+ if (pos > 0) lines.shift();
41
+
41
42
  return lines
42
43
  .filter(Boolean)
43
44
  .map((line) => {
@@ -50,7 +51,50 @@ export class MemoryStore {
50
51
  .filter(Boolean) as MemoryEvent[];
51
52
  }
52
53
 
53
- /** Count approximate token length (4 chars ≈ 1 token) */
54
+ /**
55
+ * Scrub sensitive payloads so API keys never get persisted in memory.
56
+ */
57
+ private sanitize(event: MemoryEvent): MemoryEvent {
58
+ const keys = this.sensitiveValues();
59
+ const jsonString = JSON.stringify(event);
60
+ let scrubbed = jsonString;
61
+
62
+ for (const secret of keys) {
63
+ if (!secret) continue;
64
+ if (scrubbed.includes(secret)) {
65
+ const token = this.mask(secret);
66
+ scrubbed = scrubbed.split(secret).join(token);
67
+ }
68
+ }
69
+
70
+ try {
71
+ const parsed = JSON.parse(scrubbed);
72
+ return parsed as MemoryEvent;
73
+ } catch {
74
+ return event;
75
+ }
76
+ }
77
+
78
+ private sensitiveValues(): string[] {
79
+ const interesting = [
80
+ "ANTHROPIC_API_KEY",
81
+ "GOOGLE_API_KEY",
82
+ "OPENAI_API_KEY",
83
+ "OLLAMA_HOST",
84
+ ];
85
+
86
+ const values = interesting
87
+ .map((name) => process.env[name])
88
+ .filter((value): value is string => typeof value === "string" && value.length > 0);
89
+
90
+ return Array.from(new Set(values));
91
+ }
92
+
93
+ private mask(value: string): string {
94
+ return `${value.slice(0, 3)}...${value.slice(-3)}[redacted]`;
95
+ }
96
+
97
+ /** Approximate token count kept for compat; use ContextManager for precise counts. */
54
98
  estimatedTokenCount(): number {
55
99
  if (!existsSync(this.memoryPath)) return 0;
56
100
  const stat = statSync(this.memoryPath);