@kuralle-agents/core 0.11.0 → 0.13.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.
Files changed (99) hide show
  1. package/README.md +85 -0
  2. package/dist/capabilities/LivePromptAssembler.js +1 -0
  3. package/dist/flow/collectDigression.d.ts +5 -2
  4. package/dist/flow/collectDigression.js +48 -10
  5. package/dist/flow/collectUntilComplete.js +9 -1
  6. package/dist/flow/extraction.d.ts +1 -0
  7. package/dist/flow/extraction.js +4 -0
  8. package/dist/flow/runFlow.js +63 -5
  9. package/dist/index.d.ts +9 -1
  10. package/dist/index.js +5 -0
  11. package/dist/prompts/PromptBuilder.js +7 -0
  12. package/dist/prompts/types.d.ts +2 -0
  13. package/dist/runtime/InMemoryRetrievalCache.d.ts +25 -0
  14. package/dist/runtime/InMemoryRetrievalCache.js +59 -0
  15. package/dist/runtime/KnowledgeProvider.js +6 -1
  16. package/dist/runtime/Runtime.d.ts +44 -0
  17. package/dist/runtime/Runtime.js +196 -13
  18. package/dist/runtime/TokenAccumulator.d.ts +7 -0
  19. package/dist/runtime/TokenAccumulator.js +7 -0
  20. package/dist/runtime/TraceRecorder.d.ts +33 -0
  21. package/dist/runtime/TraceRecorder.js +290 -0
  22. package/dist/runtime/buildAgentToolSurface.js +4 -0
  23. package/dist/runtime/channels/TextDriver.js +45 -22
  24. package/dist/runtime/channels/executeModelTool.d.ts +8 -1
  25. package/dist/runtime/channels/executeModelTool.js +70 -1
  26. package/dist/runtime/channels/inputBuffer.d.ts +1 -0
  27. package/dist/runtime/channels/inputBuffer.js +9 -0
  28. package/dist/runtime/closeRun.js +11 -8
  29. package/dist/runtime/compaction.d.ts +2 -0
  30. package/dist/runtime/compaction.js +3 -2
  31. package/dist/runtime/ctx.js +114 -36
  32. package/dist/runtime/durable/RunStore.d.ts +16 -0
  33. package/dist/runtime/durable/RunStore.js +6 -0
  34. package/dist/runtime/durable/SessionRunStore.d.ts +7 -2
  35. package/dist/runtime/durable/SessionRunStore.js +136 -34
  36. package/dist/runtime/durable/idempotency.d.ts +1 -0
  37. package/dist/runtime/durable/idempotency.js +3 -0
  38. package/dist/runtime/durable/replay.js +7 -2
  39. package/dist/runtime/durable/types.d.ts +11 -0
  40. package/dist/runtime/goals.d.ts +18 -0
  41. package/dist/runtime/goals.js +158 -0
  42. package/dist/runtime/grounding/knowledge.js +1 -1
  43. package/dist/runtime/handoffContinuation.d.ts +20 -0
  44. package/dist/runtime/handoffContinuation.js +30 -0
  45. package/dist/runtime/handoffOscillation.d.ts +6 -0
  46. package/dist/runtime/handoffOscillation.js +17 -0
  47. package/dist/runtime/hostLoop.js +11 -0
  48. package/dist/runtime/index.d.ts +3 -1
  49. package/dist/runtime/index.js +1 -0
  50. package/dist/runtime/openRun.d.ts +2 -0
  51. package/dist/runtime/openRun.js +46 -17
  52. package/dist/runtime/policies/limits.d.ts +1 -0
  53. package/dist/runtime/policies/limits.js +3 -0
  54. package/dist/runtime/resolveAgentWorkspace.d.ts +3 -0
  55. package/dist/runtime/resolveAgentWorkspace.js +6 -2
  56. package/dist/runtime/select.d.ts +5 -1
  57. package/dist/runtime/select.js +30 -1
  58. package/dist/runtime/turnTokenUsage.d.ts +28 -0
  59. package/dist/runtime/turnTokenUsage.js +70 -0
  60. package/dist/session/SessionStore.d.ts +6 -0
  61. package/dist/session/SessionStore.js +12 -1
  62. package/dist/session/stores/MemoryStore.d.ts +1 -1
  63. package/dist/session/stores/MemoryStore.js +16 -2
  64. package/dist/session/testing.d.ts +6 -1
  65. package/dist/session/testing.js +34 -0
  66. package/dist/session/utils.d.ts +3 -0
  67. package/dist/session/utils.js +23 -0
  68. package/dist/tools/effect/ToolExecutor.js +10 -2
  69. package/dist/tools/effect/defineTool.d.ts +3 -0
  70. package/dist/tools/effect/defineTool.js +3 -0
  71. package/dist/tools/fs/caps.d.ts +21 -0
  72. package/dist/tools/fs/caps.js +59 -0
  73. package/dist/tools/fs/createFsTool.js +68 -7
  74. package/dist/tools/fs/createShellTool.d.ts +7 -0
  75. package/dist/tools/fs/createShellTool.js +90 -0
  76. package/dist/tracing/MemoryTraceStore.d.ts +16 -0
  77. package/dist/tracing/MemoryTraceStore.js +56 -0
  78. package/dist/tracing/OtelTraceSink.d.ts +127 -0
  79. package/dist/tracing/OtelTraceSink.js +101 -0
  80. package/dist/tracing/TraceStore.d.ts +19 -0
  81. package/dist/tracing/TraceStore.js +32 -0
  82. package/dist/tracing/index.d.ts +3 -0
  83. package/dist/tracing/index.js +3 -0
  84. package/dist/tracing/testing.d.ts +3 -0
  85. package/dist/tracing/testing.js +45 -0
  86. package/dist/types/agentConfig.d.ts +4 -1
  87. package/dist/types/channel.d.ts +14 -1
  88. package/dist/types/effectTool.d.ts +6 -0
  89. package/dist/types/index.d.ts +1 -0
  90. package/dist/types/index.js +1 -0
  91. package/dist/types/run-context.d.ts +15 -2
  92. package/dist/types/session.d.ts +10 -0
  93. package/dist/types/shell.d.ts +15 -0
  94. package/dist/types/shell.js +1 -0
  95. package/dist/types/stream.d.ts +8 -0
  96. package/dist/types/trace.d.ts +50 -0
  97. package/dist/types/trace.js +1 -0
  98. package/guides/EXAMPLE_VERIFICATION.md +4 -4
  99. package/package.json +14 -4
@@ -1,5 +1,11 @@
1
1
  import type { Session } from '../types/index.js';
2
2
  import type { AuditListOptions, ConversationAuditEntry } from '../audit/types.js';
3
+ export declare class StaleWriteError extends Error {
4
+ readonly sessionId: string;
5
+ readonly expectedVersion: number;
6
+ readonly actualVersion: number;
7
+ constructor(sessionId: string, expectedVersion: number, actualVersion: number);
8
+ }
3
9
  export interface SessionListWindow {
4
10
  from?: Date;
5
11
  to?: Date;
@@ -1 +1,12 @@
1
- export {};
1
+ export class StaleWriteError extends Error {
2
+ sessionId;
3
+ expectedVersion;
4
+ actualVersion;
5
+ constructor(sessionId, expectedVersion, actualVersion) {
6
+ super(`Stale write for session ${sessionId}: expected version ${expectedVersion}, stored version is ${actualVersion}`);
7
+ this.name = 'StaleWriteError';
8
+ this.sessionId = sessionId;
9
+ this.expectedVersion = expectedVersion;
10
+ this.actualVersion = actualVersion;
11
+ }
12
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Session } from '../../types/index.js';
2
2
  import type { AuditListOptions, ConversationAuditEntry } from '../../audit/types.js';
3
- import type { SessionListWindow, SessionStore } from '../SessionStore.js';
3
+ import { type SessionListWindow, type SessionStore } from '../SessionStore.js';
4
4
  export declare class MemoryStore implements SessionStore {
5
5
  private sessions;
6
6
  get(id: string): Promise<Session | null>;
@@ -1,3 +1,4 @@
1
+ import { StaleWriteError } from '../SessionStore.js';
1
2
  export class MemoryStore {
2
3
  sessions = new Map();
3
4
  async get(id) {
@@ -5,8 +6,21 @@ export class MemoryStore {
5
6
  return session ? safeClone(session) : null;
6
7
  }
7
8
  async save(session) {
8
- session.updatedAt = new Date();
9
- this.sessions.set(session.id, safeClone(session));
9
+ const existing = this.sessions.get(session.id);
10
+ const expected = session.version ?? 0;
11
+ if (existing !== undefined) {
12
+ const stored = existing.version ?? 0;
13
+ if (stored !== expected) {
14
+ throw new StaleWriteError(session.id, expected, stored);
15
+ }
16
+ }
17
+ else if (expected !== 0) {
18
+ throw new StaleWriteError(session.id, expected, 0);
19
+ }
20
+ const toSave = safeClone(session);
21
+ toSave.updatedAt = new Date();
22
+ toSave.version = expected + 1;
23
+ this.sessions.set(session.id, toSave);
10
24
  }
11
25
  async delete(id) {
12
26
  this.sessions.delete(id);
@@ -1,7 +1,12 @@
1
- import type { SessionStore } from './SessionStore.js';
1
+ import { type SessionStore } from './SessionStore.js';
2
2
  export type SessionStoreFactory = () => SessionStore | Promise<SessionStore>;
3
3
  /**
4
4
  * Registers the shared SessionStore contract tests. Must be invoked at the
5
5
  * top level of a bun test file.
6
6
  */
7
7
  export declare function runSessionStoreContract(factory: SessionStoreFactory): void;
8
+ /**
9
+ * Registers optimistic-concurrency (CAS) contract tests for SessionStore adapters.
10
+ * Two concurrent saves with the same expected version: exactly one succeeds.
11
+ */
12
+ export declare function runSessionStoreCasContract(factory: SessionStoreFactory): void;
@@ -21,6 +21,7 @@
21
21
  * `bun:test` into runtime bundles.
22
22
  */
23
23
  import { describe, test, expect, beforeEach } from 'bun:test';
24
+ import { StaleWriteError } from './SessionStore.js';
24
25
  const buildSession = (overrides = {}) => {
25
26
  const now = new Date();
26
27
  return {
@@ -120,3 +121,36 @@ export function runSessionStoreContract(factory) {
120
121
  });
121
122
  });
122
123
  }
124
+ /**
125
+ * Registers optimistic-concurrency (CAS) contract tests for SessionStore adapters.
126
+ * Two concurrent saves with the same expected version: exactly one succeeds.
127
+ */
128
+ export function runSessionStoreCasContract(factory) {
129
+ describe('SessionStore CAS contract', () => {
130
+ let store;
131
+ beforeEach(async () => {
132
+ store = await factory();
133
+ });
134
+ test('concurrent save with same expected version rejects one writer with StaleWriteError', async () => {
135
+ const session = buildSession({ id: 'cas-sess' });
136
+ await store.save(session);
137
+ const firstRead = (await store.get('cas-sess'));
138
+ const secondRead = (await store.get('cas-sess'));
139
+ expect(firstRead.version).toBe(1);
140
+ expect(secondRead.version).toBe(1);
141
+ firstRead.workingMemory = { writer: 'a' };
142
+ secondRead.workingMemory = { writer: 'b' };
143
+ const results = await Promise.allSettled([
144
+ store.save(firstRead),
145
+ store.save(secondRead),
146
+ ]);
147
+ const fulfilled = results.filter((r) => r.status === 'fulfilled');
148
+ const rejected = results.filter((r) => r.status === 'rejected');
149
+ expect(fulfilled).toHaveLength(1);
150
+ expect(rejected).toHaveLength(1);
151
+ expect(rejected[0].reason).toBeInstanceOf(StaleWriteError);
152
+ const final = await store.get('cas-sess');
153
+ expect(final?.version).toBe(2);
154
+ });
155
+ });
156
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Session } from '../types/index.js';
2
+ import { type SessionStore } from './SessionStore.js';
2
3
  /**
3
4
  * Restores a Session from its serialized form.
4
5
  *
@@ -17,3 +18,5 @@ import type { Session } from '../types/index.js';
17
18
  * untouched in the key fields — callers are expected to have validated shape.
18
19
  */
19
20
  export declare function reviveSession(raw: unknown): Session;
21
+ /** Apply a session mutation to the latest snapshot and retry it after CAS conflicts. */
22
+ export declare function mutateSessionWithRetry(store: SessionStore, sessionId: string, mutator: (session: Session) => void | Promise<void>): Promise<Session>;
@@ -1,3 +1,5 @@
1
+ import { StaleWriteError } from './SessionStore.js';
2
+ const CAS_RETRIES = 8;
1
3
  /**
2
4
  * Restores a Session from its serialized form.
3
5
  *
@@ -45,3 +47,24 @@ export function reviveSession(raw) {
45
47
  ]));
46
48
  return session;
47
49
  }
50
+ /** Apply a session mutation to the latest snapshot and retry it after CAS conflicts. */
51
+ export async function mutateSessionWithRetry(store, sessionId, mutator) {
52
+ for (let attempt = 0; attempt < CAS_RETRIES; attempt++) {
53
+ const session = await store.get(sessionId);
54
+ if (!session) {
55
+ throw new Error(`Session not found: ${sessionId}`);
56
+ }
57
+ await mutator(session);
58
+ try {
59
+ await store.save(session);
60
+ return (await store.get(sessionId)) ?? session;
61
+ }
62
+ catch (error) {
63
+ if (error instanceof StaleWriteError && attempt < CAS_RETRIES - 1) {
64
+ continue;
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ throw new Error(`CAS retry limit exceeded for session: ${sessionId}`);
70
+ }
@@ -26,7 +26,10 @@ export class CoreToolExecutor {
26
26
  return this.tools.get(name);
27
27
  }
28
28
  async execute(args) {
29
- if (!this.parallelExecution) {
29
+ const registryDef = this.tools.get(args.name);
30
+ const def = args.def ?? registryDef;
31
+ const parallelSafe = def?.parallelSafe === true || def?.replay === false;
32
+ if (!this.parallelExecution && !parallelSafe) {
30
33
  return this.withSerialGate(() => this.executeInner(args));
31
34
  }
32
35
  return this.executeInner(args);
@@ -114,7 +117,12 @@ export class CoreToolExecutor {
114
117
  interimTimer.unref();
115
118
  }
116
119
  }
117
- const executePromise = Promise.resolve(def.execute(sanitizedArgs, toolCtx)).then(async (result) => {
120
+ const executeCtx = toolCtx
121
+ ? { ...toolCtx, abortSignal }
122
+ : abortSignal
123
+ ? { abortSignal }
124
+ : undefined;
125
+ const executePromise = Promise.resolve(def.execute(sanitizedArgs, executeCtx)).then(async (result) => {
118
126
  if (result && typeof result[Symbol.asyncIterator] === 'function') {
119
127
  const chunks = [];
120
128
  for await (const chunk of result) {
@@ -18,6 +18,9 @@ export declare function defineTool<S extends z.ZodTypeAny | StandardSchemaV1 | u
18
18
  /** @deprecated Use `interimAfterMs`. */
19
19
  estimatedDurationMs?: number;
20
20
  timeoutMs?: number;
21
+ replay?: boolean;
22
+ parallelSafe?: boolean;
23
+ idempotencyKey?: (args: InferToolInput<S>) => string;
21
24
  execute: (args: InferToolInput<S>, ctx?: ToolContext) => Promise<R> | AsyncIterable<R>;
22
25
  }): Tool<InferToolInput<S>, R>;
23
26
  export declare function toolToAiSdk<TInput = unknown, TOutput = unknown>(def: Tool<TInput, TOutput>): AiTool<TInput, TOutput>;
@@ -10,6 +10,9 @@ export function defineTool(config) {
10
10
  interim: config.interim ?? config.filler,
11
11
  interimAfterMs: config.interimAfterMs ?? config.estimatedDurationMs,
12
12
  timeoutMs: config.timeoutMs,
13
+ replay: config.replay,
14
+ parallelSafe: config.parallelSafe,
15
+ idempotencyKey: config.idempotencyKey,
13
16
  execute: config.execute,
14
17
  };
15
18
  }
@@ -0,0 +1,21 @@
1
+ export declare const MAX_READ_LINES = 2000;
2
+ export declare const MAX_READ_BYTES: number;
3
+ export declare const MAX_GREP_HITS = 200;
4
+ export declare const MAX_GREP_LINE_LEN = 500;
5
+ export declare const MAX_LIST_ENTRIES = 1000;
6
+ export declare const MAX_SHELL_OUTPUT_BYTES: number;
7
+ export declare function applyReadWindow(content: string, offset?: number, limit?: number): {
8
+ content: string;
9
+ truncated: boolean;
10
+ note?: string;
11
+ };
12
+ export declare function capGrepHits<T extends {
13
+ text: string;
14
+ }>(hits: T[]): {
15
+ hits: T[];
16
+ truncated: boolean;
17
+ };
18
+ export declare function capList<T>(entries: T[]): {
19
+ entries: T[];
20
+ truncated: boolean;
21
+ };
@@ -0,0 +1,59 @@
1
+ export const MAX_READ_LINES = 2000;
2
+ export const MAX_READ_BYTES = 50 * 1024;
3
+ export const MAX_GREP_HITS = 200;
4
+ export const MAX_GREP_LINE_LEN = 500;
5
+ export const MAX_LIST_ENTRIES = 1000;
6
+ export const MAX_SHELL_OUTPUT_BYTES = 50 * 1024;
7
+ export function applyReadWindow(content, offset, limit) {
8
+ const allLines = content.split('\n');
9
+ let lines = allLines;
10
+ let truncated = false;
11
+ const notes = [];
12
+ // `offset` is a 1-indexed start line. 0, 1, and undefined all mean "from the
13
+ // start" — models routinely fill an optional numeric arg with 0, and that must
14
+ // not silently empty the read.
15
+ if (offset !== undefined && offset > 1) {
16
+ lines = lines.slice(offset - 1);
17
+ truncated = true;
18
+ }
19
+ // `limit` caps the returned line count only when positive. 0 and undefined
20
+ // both mean "no explicit limit" (still bounded by MAX_READ_LINES below).
21
+ if (limit !== undefined && limit > 0 && lines.length > limit) {
22
+ lines = lines.slice(0, limit);
23
+ truncated = true;
24
+ }
25
+ if (lines.length > MAX_READ_LINES) {
26
+ lines = lines.slice(0, MAX_READ_LINES);
27
+ truncated = true;
28
+ notes.push(`truncated at ${MAX_READ_LINES} lines; use offset/limit`);
29
+ }
30
+ let result = lines.join('\n');
31
+ if (result.length > MAX_READ_BYTES) {
32
+ result = result.slice(0, MAX_READ_BYTES);
33
+ truncated = true;
34
+ if (!notes.some((n) => n.includes('bytes'))) {
35
+ notes.push(`truncated at ${MAX_READ_BYTES} bytes; use offset/limit`);
36
+ }
37
+ }
38
+ if (!truncated) {
39
+ return { content: result, truncated: false };
40
+ }
41
+ const note = notes.length > 0 ? notes.join('; ') : 'truncated; use offset/limit';
42
+ return { content: result, truncated: true, note };
43
+ }
44
+ export function capGrepHits(hits) {
45
+ const truncated = hits.length > MAX_GREP_HITS;
46
+ const capped = hits.slice(0, MAX_GREP_HITS).map((hit) => {
47
+ if (hit.text.length <= MAX_GREP_LINE_LEN)
48
+ return hit;
49
+ return {
50
+ ...hit,
51
+ text: `${hit.text.slice(0, MAX_GREP_LINE_LEN)}…`,
52
+ };
53
+ });
54
+ return { hits: capped, truncated };
55
+ }
56
+ export function capList(entries) {
57
+ const truncated = entries.length > MAX_LIST_ENTRIES;
58
+ return { entries: entries.slice(0, MAX_LIST_ENTRIES), truncated };
59
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { defineTool } from '../effect/defineTool.js';
3
3
  import { fsErrorCode } from '../../types/filesystem.js';
4
+ import { applyReadWindow, capGrepHits, capList } from './caps.js';
4
5
  const DEFAULT_READ_ONLY = true;
5
6
  const workspaceInput = z.object({
6
7
  op: z.enum(['ls', 'cat', 'grep', 'find', 'read', 'write', 'edit']),
@@ -12,6 +13,9 @@ const workspaceInput = z.object({
12
13
  content: z.string().optional(),
13
14
  find: z.string().optional(),
14
15
  replace: z.string().optional(),
16
+ offset: z.number().optional(),
17
+ limit: z.number().optional(),
18
+ replaceAll: z.boolean().optional(),
15
19
  });
16
20
  function normalizeFsPath(path, fallback = '/') {
17
21
  if (!path || path.trim() === '')
@@ -32,6 +36,32 @@ function assertWritable(readOnly, path) {
32
36
  if (readOnly)
33
37
  throw eroFs(path);
34
38
  }
39
+ const GREP_FLAG_ORDER = ['g', 'i', 'm', 's'];
40
+ function parseGrepFlags(flags) {
41
+ if (!flags)
42
+ return undefined;
43
+ const allowed = new Set(GREP_FLAG_ORDER);
44
+ const seen = new Set();
45
+ const out = [];
46
+ for (const ch of flags) {
47
+ if (allowed.has(ch) && !seen.has(ch)) {
48
+ seen.add(ch);
49
+ out.push(ch);
50
+ }
51
+ }
52
+ return out.length > 0 ? out.join('') : undefined;
53
+ }
54
+ function countOccurrences(haystack, needle) {
55
+ if (needle.length === 0)
56
+ return 0;
57
+ let count = 0;
58
+ let pos = 0;
59
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
60
+ count++;
61
+ pos += needle.length;
62
+ }
63
+ return count;
64
+ }
35
65
  async function listFiles(fs, root) {
36
66
  const normalized = root === '' ? '/' : root;
37
67
  const out = [];
@@ -62,7 +92,7 @@ async function listFiles(fs, root) {
62
92
  async function grepFiles(fs, pattern, root, flags) {
63
93
  let re;
64
94
  try {
65
- re = new RegExp(pattern, flags?.includes('i') ? 'i' : undefined);
95
+ re = new RegExp(pattern, parseGrepFlags(flags));
66
96
  }
67
97
  catch {
68
98
  throw new Error(`EINVAL: invalid grep pattern '${pattern}'`);
@@ -84,6 +114,7 @@ async function grepFiles(fs, pattern, root, flags) {
84
114
  }
85
115
  const lines = content.split('\n');
86
116
  for (let i = 0; i < lines.length; i++) {
117
+ re.lastIndex = 0;
87
118
  if (re.test(lines[i])) {
88
119
  hits.push({ path: filePath, line: i + 1, text: lines[i] });
89
120
  }
@@ -105,6 +136,7 @@ async function grepFiles(fs, pattern, root, flags) {
105
136
  }
106
137
  const lines = content.split('\n');
107
138
  for (let i = 0; i < lines.length; i++) {
139
+ re.lastIndex = 0;
108
140
  if (re.test(lines[i])) {
109
141
  hits.push({ path: filePath, line: i + 1, text: lines[i] });
110
142
  }
@@ -133,19 +165,30 @@ export function createFsTool(opts) {
133
165
  switch (args.op) {
134
166
  case 'ls': {
135
167
  const path = normalizeFsPath(args.path);
136
- const entries = await fs.readdirWithFileTypes(path);
168
+ const rawEntries = await fs.readdirWithFileTypes(path);
169
+ const { entries, truncated } = capList(rawEntries);
137
170
  return {
138
171
  op: args.op,
139
172
  ok: true,
140
173
  path,
141
174
  entries,
175
+ ...(truncated ? { truncated: true } : {}),
142
176
  };
143
177
  }
144
178
  case 'cat':
145
179
  case 'read': {
146
180
  const path = requireField(args, 'path', args.op);
147
- const content = await fs.readFile(path);
148
- return { op: args.op, ok: true, path, content };
181
+ const raw = await fs.readFile(path);
182
+ const windowed = applyReadWindow(raw, args.offset, args.limit);
183
+ return {
184
+ op: args.op,
185
+ ok: true,
186
+ path,
187
+ content: windowed.content,
188
+ ...(windowed.truncated
189
+ ? { truncated: true, note: windowed.note }
190
+ : {}),
191
+ };
149
192
  }
150
193
  case 'find': {
151
194
  const root = normalizeFsPath(args.root);
@@ -155,24 +198,28 @@ export function createFsTool(opts) {
155
198
  const normalizedRoot = root === '/' ? '/' : root.replace(/\/$/, '');
156
199
  return p === normalizedRoot || p.startsWith(`${normalizedRoot}/`);
157
200
  });
201
+ const { entries: cappedPaths, truncated } = capList(rooted);
158
202
  return {
159
203
  op: args.op,
160
204
  ok: true,
161
205
  root,
162
206
  glob,
163
- paths: rooted,
207
+ paths: cappedPaths,
208
+ ...(truncated ? { truncated: true } : {}),
164
209
  };
165
210
  }
166
211
  case 'grep': {
167
212
  const pattern = requireField(args, 'pattern', args.op);
168
213
  const path = normalizeFsPath(args.path);
169
- const hits = await grepFiles(fs, pattern, path, args.flags);
214
+ const rawHits = await grepFiles(fs, pattern, path, args.flags);
215
+ const { hits, truncated } = capGrepHits(rawHits);
170
216
  return {
171
217
  op: args.op,
172
218
  ok: true,
173
219
  pattern,
174
220
  path,
175
221
  hits,
222
+ ...(truncated ? { truncated: true } : {}),
176
223
  };
177
224
  }
178
225
  case 'write': {
@@ -188,9 +235,23 @@ export function createFsTool(opts) {
188
235
  const replace = requireField(args, 'replace', args.op);
189
236
  assertWritable(readOnly, path);
190
237
  const current = await fs.readFile(path);
191
- if (!current.includes(find)) {
238
+ const occurrences = countOccurrences(current, find);
239
+ if (occurrences === 0) {
192
240
  throw new Error(`ENOENT: find string not found in file, edit '${path}'`);
193
241
  }
242
+ if (args.replaceAll) {
243
+ const next = current.replaceAll(find, replace);
244
+ await fs.writeFile(path, next);
245
+ return {
246
+ op: args.op,
247
+ ok: true,
248
+ path,
249
+ replacements: occurrences,
250
+ };
251
+ }
252
+ if (occurrences > 1) {
253
+ throw new Error(`EAMBIG: ${occurrences} occurrences of find string in '${path}'; add surrounding context or use replaceAll`);
254
+ }
194
255
  const next = current.replace(find, replace);
195
256
  await fs.writeFile(path, next);
196
257
  return { op: args.op, ok: true, path };
@@ -0,0 +1,7 @@
1
+ import type { AnyTool } from '../../types/effectTool.js';
2
+ import type { Shell } from '../../types/shell.js';
3
+ export interface CreateShellToolOptions {
4
+ shell: Shell;
5
+ timeoutMs?: number;
6
+ }
7
+ export declare function createShellTool(opts: CreateShellToolOptions): AnyTool;
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+ import { defineTool } from '../effect/defineTool.js';
3
+ import { MAX_SHELL_OUTPUT_BYTES } from './caps.js';
4
+ const bashInput = z.object({
5
+ command: z.string(),
6
+ timeout: z.number().optional(),
7
+ });
8
+ const TIMEOUT_EXIT_CODE = 124;
9
+ function isTimeoutError(error) {
10
+ if (!(error instanceof Error))
11
+ return false;
12
+ const msg = error.message.toLowerCase();
13
+ return msg.includes('timeout') || msg.includes('timed out');
14
+ }
15
+ function capShellOutput(stdout, stderr) {
16
+ const combined = stdout.length + stderr.length;
17
+ if (combined <= MAX_SHELL_OUTPUT_BYTES) {
18
+ return { stdout, stderr };
19
+ }
20
+ const note = `\n[kuralle] output truncated at ${MAX_SHELL_OUTPUT_BYTES} bytes`;
21
+ const budget = MAX_SHELL_OUTPUT_BYTES - note.length;
22
+ if (budget <= 0) {
23
+ return { stdout: '', stderr: note.trim() };
24
+ }
25
+ if (stdout.length >= budget) {
26
+ return { stdout: stdout.slice(0, budget), stderr: note.trim() };
27
+ }
28
+ const stderrBudget = budget - stdout.length;
29
+ return {
30
+ stdout,
31
+ stderr: `${stderr.slice(0, stderrBudget)}${note}`,
32
+ };
33
+ }
34
+ export function createShellTool(opts) {
35
+ const defaultTimeoutMs = opts.timeoutMs;
36
+ return defineTool({
37
+ name: 'bash',
38
+ description: 'Run a shell command in the agent workspace. Returns stdout, stderr, exitCode. Output is truncated when large.',
39
+ replay: false,
40
+ input: bashInput,
41
+ execute: async (args, ctx) => {
42
+ const timeoutMs = args.timeout !== undefined ? args.timeout * 1000 : defaultTimeoutMs;
43
+ if (ctx?.abortSignal?.aborted) {
44
+ throw new DOMException('Aborted', 'AbortError');
45
+ }
46
+ let result;
47
+ try {
48
+ result = await opts.shell.exec(args.command, {
49
+ timeoutMs,
50
+ signal: ctx?.abortSignal,
51
+ });
52
+ }
53
+ catch (error) {
54
+ if (ctx?.abortSignal?.aborted) {
55
+ throw error;
56
+ }
57
+ if (isTimeoutError(error)) {
58
+ const seconds = timeoutMs !== undefined ? Math.round(timeoutMs / 1000) : args.timeout ?? 0;
59
+ return {
60
+ op: 'bash',
61
+ ok: false,
62
+ stdout: '',
63
+ stderr: `[kuralle] command timed out after ${seconds}s`,
64
+ exitCode: TIMEOUT_EXIT_CODE,
65
+ };
66
+ }
67
+ throw error;
68
+ }
69
+ if (result.exitCode === TIMEOUT_EXIT_CODE) {
70
+ const seconds = timeoutMs !== undefined ? Math.round(timeoutMs / 1000) : args.timeout ?? 0;
71
+ return {
72
+ op: 'bash',
73
+ ok: false,
74
+ stdout: result.stdout,
75
+ stderr: result.stderr ||
76
+ `[kuralle] command timed out after ${seconds}s`,
77
+ exitCode: TIMEOUT_EXIT_CODE,
78
+ };
79
+ }
80
+ const capped = capShellOutput(result.stdout, result.stderr);
81
+ return {
82
+ op: 'bash',
83
+ ok: result.exitCode === 0,
84
+ stdout: capped.stdout,
85
+ stderr: capped.stderr,
86
+ exitCode: result.exitCode,
87
+ };
88
+ },
89
+ });
90
+ }
@@ -0,0 +1,16 @@
1
+ import type { AgentSpan, AgentTrace } from '../types/trace.js';
2
+ import { type TraceListWindow, type TraceStore } from './TraceStore.js';
3
+ export interface MemoryTraceStoreOptions {
4
+ retentionMs?: number;
5
+ }
6
+ export declare class MemoryTraceStore implements TraceStore {
7
+ private readonly options;
8
+ private readonly spans;
9
+ constructor(options?: MemoryTraceStoreOptions);
10
+ write(span: AgentSpan): void;
11
+ putSpan(span: AgentSpan): void;
12
+ getTrace(traceId: string): Promise<AgentTrace | null>;
13
+ listTraces(sessionId: string, window?: TraceListWindow): Promise<AgentTrace[]>;
14
+ cleanup(maxAgeMs: number): Promise<number>;
15
+ private cleanupSync;
16
+ }
@@ -0,0 +1,56 @@
1
+ import { cloneSpan, traceFromSpans, } from './TraceStore.js';
2
+ export class MemoryTraceStore {
3
+ options;
4
+ spans = new Map();
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ }
8
+ write(span) {
9
+ this.putSpan(span);
10
+ }
11
+ putSpan(span) {
12
+ let trace = this.spans.get(span.traceId);
13
+ if (!trace) {
14
+ trace = new Map();
15
+ this.spans.set(span.traceId, trace);
16
+ }
17
+ trace.set(span.spanId, cloneSpan(span));
18
+ if (this.options.retentionMs !== undefined)
19
+ this.cleanupSync(this.options.retentionMs);
20
+ }
21
+ async getTrace(traceId) {
22
+ const spans = this.spans.get(traceId);
23
+ return traceFromSpans(spans ? [...spans.values()] : []);
24
+ }
25
+ async listTraces(sessionId, window) {
26
+ const traces = [...this.spans.values()]
27
+ .map((spans) => traceFromSpans([...spans.values()]))
28
+ .filter((trace) => trace !== null)
29
+ .filter((trace) => trace.sessionId === sessionId)
30
+ .filter((trace) => inWindow(trace.startedAt, window))
31
+ .sort((a, b) => b.startedAt - a.startedAt);
32
+ return window?.limit === undefined ? traces : traces.slice(0, window.limit);
33
+ }
34
+ async cleanup(maxAgeMs) {
35
+ return this.cleanupSync(maxAgeMs);
36
+ }
37
+ cleanupSync(maxAgeMs) {
38
+ const cutoff = Date.now() - maxAgeMs;
39
+ let removed = 0;
40
+ for (const [traceId, spans] of this.spans) {
41
+ const root = [...spans.values()].find((span) => span.kind === 'turn');
42
+ if ((root?.endTime ?? root?.startTime ?? 0) < cutoff) {
43
+ this.spans.delete(traceId);
44
+ removed += 1;
45
+ }
46
+ }
47
+ return removed;
48
+ }
49
+ }
50
+ function inWindow(timestamp, window) {
51
+ if (window?.from && timestamp < window.from.getTime())
52
+ return false;
53
+ if (window?.to && timestamp > window.to.getTime())
54
+ return false;
55
+ return true;
56
+ }