@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.
- package/README.md +85 -0
- package/dist/capabilities/LivePromptAssembler.js +1 -0
- package/dist/flow/collectDigression.d.ts +5 -2
- package/dist/flow/collectDigression.js +48 -10
- package/dist/flow/collectUntilComplete.js +9 -1
- package/dist/flow/extraction.d.ts +1 -0
- package/dist/flow/extraction.js +4 -0
- package/dist/flow/runFlow.js +63 -5
- package/dist/index.d.ts +9 -1
- package/dist/index.js +5 -0
- package/dist/prompts/PromptBuilder.js +7 -0
- package/dist/prompts/types.d.ts +2 -0
- package/dist/runtime/InMemoryRetrievalCache.d.ts +25 -0
- package/dist/runtime/InMemoryRetrievalCache.js +59 -0
- package/dist/runtime/KnowledgeProvider.js +6 -1
- package/dist/runtime/Runtime.d.ts +44 -0
- package/dist/runtime/Runtime.js +196 -13
- package/dist/runtime/TokenAccumulator.d.ts +7 -0
- package/dist/runtime/TokenAccumulator.js +7 -0
- package/dist/runtime/TraceRecorder.d.ts +33 -0
- package/dist/runtime/TraceRecorder.js +290 -0
- package/dist/runtime/buildAgentToolSurface.js +4 -0
- package/dist/runtime/channels/TextDriver.js +45 -22
- package/dist/runtime/channels/executeModelTool.d.ts +8 -1
- package/dist/runtime/channels/executeModelTool.js +70 -1
- package/dist/runtime/channels/inputBuffer.d.ts +1 -0
- package/dist/runtime/channels/inputBuffer.js +9 -0
- package/dist/runtime/closeRun.js +11 -8
- package/dist/runtime/compaction.d.ts +2 -0
- package/dist/runtime/compaction.js +3 -2
- package/dist/runtime/ctx.js +114 -36
- package/dist/runtime/durable/RunStore.d.ts +16 -0
- package/dist/runtime/durable/RunStore.js +6 -0
- package/dist/runtime/durable/SessionRunStore.d.ts +7 -2
- package/dist/runtime/durable/SessionRunStore.js +136 -34
- package/dist/runtime/durable/idempotency.d.ts +1 -0
- package/dist/runtime/durable/idempotency.js +3 -0
- package/dist/runtime/durable/replay.js +7 -2
- package/dist/runtime/durable/types.d.ts +11 -0
- package/dist/runtime/goals.d.ts +18 -0
- package/dist/runtime/goals.js +158 -0
- package/dist/runtime/grounding/knowledge.js +1 -1
- package/dist/runtime/handoffContinuation.d.ts +20 -0
- package/dist/runtime/handoffContinuation.js +30 -0
- package/dist/runtime/handoffOscillation.d.ts +6 -0
- package/dist/runtime/handoffOscillation.js +17 -0
- package/dist/runtime/hostLoop.js +11 -0
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/openRun.d.ts +2 -0
- package/dist/runtime/openRun.js +46 -17
- package/dist/runtime/policies/limits.d.ts +1 -0
- package/dist/runtime/policies/limits.js +3 -0
- package/dist/runtime/resolveAgentWorkspace.d.ts +3 -0
- package/dist/runtime/resolveAgentWorkspace.js +6 -2
- package/dist/runtime/select.d.ts +5 -1
- package/dist/runtime/select.js +30 -1
- package/dist/runtime/turnTokenUsage.d.ts +28 -0
- package/dist/runtime/turnTokenUsage.js +70 -0
- package/dist/session/SessionStore.d.ts +6 -0
- package/dist/session/SessionStore.js +12 -1
- package/dist/session/stores/MemoryStore.d.ts +1 -1
- package/dist/session/stores/MemoryStore.js +16 -2
- package/dist/session/testing.d.ts +6 -1
- package/dist/session/testing.js +34 -0
- package/dist/session/utils.d.ts +3 -0
- package/dist/session/utils.js +23 -0
- package/dist/tools/effect/ToolExecutor.js +10 -2
- package/dist/tools/effect/defineTool.d.ts +3 -0
- package/dist/tools/effect/defineTool.js +3 -0
- package/dist/tools/fs/caps.d.ts +21 -0
- package/dist/tools/fs/caps.js +59 -0
- package/dist/tools/fs/createFsTool.js +68 -7
- package/dist/tools/fs/createShellTool.d.ts +7 -0
- package/dist/tools/fs/createShellTool.js +90 -0
- package/dist/tracing/MemoryTraceStore.d.ts +16 -0
- package/dist/tracing/MemoryTraceStore.js +56 -0
- package/dist/tracing/OtelTraceSink.d.ts +127 -0
- package/dist/tracing/OtelTraceSink.js +101 -0
- package/dist/tracing/TraceStore.d.ts +19 -0
- package/dist/tracing/TraceStore.js +32 -0
- package/dist/tracing/index.d.ts +3 -0
- package/dist/tracing/index.js +3 -0
- package/dist/tracing/testing.d.ts +3 -0
- package/dist/tracing/testing.js +45 -0
- package/dist/types/agentConfig.d.ts +4 -1
- package/dist/types/channel.d.ts +14 -1
- package/dist/types/effectTool.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/run-context.d.ts +15 -2
- package/dist/types/session.d.ts +10 -0
- package/dist/types/shell.d.ts +15 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/stream.d.ts +8 -0
- package/dist/types/trace.d.ts +50 -0
- package/dist/types/trace.js +1 -0
- package/guides/EXAMPLE_VERIFICATION.md +4 -4
- 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
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
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;
|
package/dist/session/testing.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/session/utils.d.ts
CHANGED
|
@@ -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>;
|
package/dist/session/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
148
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|