@theokit/sdk 4.2.7 → 4.2.8
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/CHANGELOG.md +6 -0
- package/claude-template/CLAUDE.md +16 -0
- package/claude-template/dot-claude/skills/theokit-auth/SKILL.md +102 -0
- package/claude-template/dot-claude/skills/theokit-client/SKILL.md +58 -0
- package/claude-template/dot-claude/skills/theokit-compaction/SKILL.md +102 -0
- package/claude-template/dot-claude/skills/theokit-concurrency/SKILL.md +68 -0
- package/claude-template/dot-claude/skills/theokit-filesystem/SKILL.md +74 -0
- package/claude-template/dot-claude/skills/theokit-messages/SKILL.md +58 -0
- package/claude-template/dot-claude/skills/theokit-models/SKILL.md +79 -0
- package/claude-template/dot-claude/skills/theokit-path-safety/SKILL.md +60 -0
- package/claude-template/dot-claude/skills/theokit-persistence/SKILL.md +85 -0
- package/claude-template/dot-claude/skills/theokit-project/SKILL.md +55 -0
- package/claude-template/dot-claude/skills/theokit-retry/SKILL.md +50 -0
- package/claude-template/dot-claude/skills/theokit-sandbox/SKILL.md +93 -0
- package/claude-template/dot-claude/skills/theokit-sanitize/SKILL.md +66 -0
- package/claude-template/dot-claude/skills/theokit-skills/SKILL.md +68 -0
- package/claude-template/dot-claude/skills/theokit-subagents/SKILL.md +109 -0
- package/claude-template/dot-claude/skills/theokit-task-store/SKILL.md +75 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat(init-claude): the scaffolded `.claude/` template now covers **every public `@theokit/sdk` subpath**. Added 16 per-module skills — models, subagents (`/a2a` + tool-scope), retry, task-store, sandbox, compaction, messages, auth (`/server/auth` + errors-envelope), sanitize, skills, path-safety, concurrency, persistence, client, filesystem, project — each authored against the shipped type declarations (verified signatures: `Retry.create` executor, `Semaphore.create`, `SubAgent.create`, `Auth.create`, `sanitizeToolInput`, …). The `claude-template-no-drift` gate covers the expanded set.
|
|
8
|
+
|
|
3
9
|
## 4.2.7
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -24,6 +24,22 @@ These skills inject TheoKit knowledge automatically when you edit files matching
|
|
|
24
24
|
| `theokit-config` | `.theokit/**`, `config.*`, `theo.config.*` |
|
|
25
25
|
| `theokit-streaming` | `*stream*`, `*Stream*`, `*SDKMessage*` |
|
|
26
26
|
| `theokit-budget` | `*budget*`, `*Budget*`, `*cost*`, `*token*` |
|
|
27
|
+
| `theokit-models` | `*model*`, `*Model*` |
|
|
28
|
+
| `theokit-subagents` | `*subagent*`, `*a2a*`, `*delegat*` |
|
|
29
|
+
| `theokit-retry` | `*retry*`, `*Retry*` |
|
|
30
|
+
| `theokit-task-store` | `*task-store*`, `*taskstore*`, `*TaskStore*` |
|
|
31
|
+
| `theokit-sandbox` | `*sandbox*`, `*Sandbox*` |
|
|
32
|
+
| `theokit-compaction` | `*compact*`, `*Compact*` |
|
|
33
|
+
| `theokit-messages` | `*message*`, `*Message*` |
|
|
34
|
+
| `theokit-auth` | `*auth*`, `*Auth*`, `*envelope*` |
|
|
35
|
+
| `theokit-sanitize` | `*sanitize*`, `*Sanitize*` |
|
|
36
|
+
| `theokit-skills` | `*skill*`, `*Skill*` |
|
|
37
|
+
| `theokit-path-safety` | `*path-safety*`, `*pathsafety*` |
|
|
38
|
+
| `theokit-concurrency` | `*concurren*`, `*semaphore*`, `*Semaphore*` |
|
|
39
|
+
| `theokit-persistence` | `*persist*`, `*Persist*` |
|
|
40
|
+
| `theokit-client` | `*client*`, `*Client*` |
|
|
41
|
+
| `theokit-filesystem` | `*filesystem*`, `*Filesystem*` |
|
|
42
|
+
| `theokit-project` | `*project*`, `*Project*` |
|
|
27
43
|
|
|
28
44
|
### Settings
|
|
29
45
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*auth*"
|
|
5
|
+
- "**/*Auth*"
|
|
6
|
+
- "**/*envelope*"
|
|
7
|
+
description: TheoKit SDK server auth — Auth.create orchestrator, validateReturnTo, and the cross-layer error envelope
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TheoKit Server Auth
|
|
11
|
+
|
|
12
|
+
Server-side auth orchestrator and the cross-layer error envelope. These live
|
|
13
|
+
under the `@theokit/sdk/server/*` sub-paths (not the main barrel). Concrete
|
|
14
|
+
OAuth/email providers ship in opt-in `@theokit/auth-*` packages — the SDK only
|
|
15
|
+
defines the orchestrator contract.
|
|
16
|
+
|
|
17
|
+
## `Auth.create` — session + provider orchestrator
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import {
|
|
21
|
+
Auth,
|
|
22
|
+
validateReturnTo,
|
|
23
|
+
AuthConfigError,
|
|
24
|
+
AuthProviderNotFoundError,
|
|
25
|
+
AuthCallbackError,
|
|
26
|
+
AuthCancelledError,
|
|
27
|
+
AuthSecretTooShortError,
|
|
28
|
+
} from "@theokit/sdk/server/auth";
|
|
29
|
+
import type {
|
|
30
|
+
AuthProvider,
|
|
31
|
+
SessionManager,
|
|
32
|
+
AuthOrchestrator,
|
|
33
|
+
} from "@theokit/sdk/server/auth";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`Auth.create(opts)` returns an `AuthOrchestrator<TSession>` with 5 methods.
|
|
37
|
+
`providers` is optional — an empty list is the manual-`signIn`-only escape hatch.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const auth: AuthOrchestrator<Session> = Auth.create<Session>({
|
|
41
|
+
session, // your SessionManager<Session> implementation
|
|
42
|
+
providers: [githubProvider], // AuthProvider<Profile>[] from a @theokit/auth-* package
|
|
43
|
+
onSignIn: async ({ profile, provider }) => {
|
|
44
|
+
return toSession(profile, provider); // returns the TSession to persist
|
|
45
|
+
},
|
|
46
|
+
onSignOut: async (session) => {
|
|
47
|
+
/* revoke, audit, etc. */
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// OAuth flow (node:http req/res):
|
|
52
|
+
const redirect = await auth.startSignIn("github", req, { returnTo: "/dashboard" });
|
|
53
|
+
const { session, returnTo } = await auth.finishSignIn("github", req, res); // rotates session id (OWASP A07)
|
|
54
|
+
const current = await auth.getSession(req);
|
|
55
|
+
await auth.signOut(res);
|
|
56
|
+
|
|
57
|
+
// Escape hatch — persist a session directly, skipping the OAuth flow:
|
|
58
|
+
const s = await auth.signIn(externalProfile, "github", req, res);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## `validateReturnTo` — open-redirect guard (OWASP A01)
|
|
62
|
+
|
|
63
|
+
Returns a safe same-origin path. Cross-origin, protocol-relative (`//evil.com`),
|
|
64
|
+
empty, and defensive cases all collapse to `"/"`.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const safe = validateReturnTo(returnTo, new URL("https://app.example.com"));
|
|
68
|
+
// "/dashboard" -> kept; "https://evil.com" -> "/"; undefined -> "/"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Typed errors: `AuthConfigError`, `AuthProviderNotFoundError`, `AuthCallbackError`,
|
|
72
|
+
`AuthCancelledError`, `AuthSecretTooShortError`.
|
|
73
|
+
|
|
74
|
+
## Error envelope — cross-layer boundary translation
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import {
|
|
78
|
+
toEnvelope,
|
|
79
|
+
fromEnvelope,
|
|
80
|
+
MemoryAdapterError,
|
|
81
|
+
} from "@theokit/sdk/server/errors-envelope";
|
|
82
|
+
import type {
|
|
83
|
+
TheokitErrorEnvelope,
|
|
84
|
+
TheokitErrorCode,
|
|
85
|
+
} from "@theokit/sdk/server/errors-envelope";
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`toEnvelope` translates any SDK error (or arbitrary thrown value) into the wire
|
|
89
|
+
envelope at egress; `fromEnvelope` reconstructs SDK class identity at ingress so
|
|
90
|
+
`instanceof` checks keep working across an IPC/serialization boundary.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
try {
|
|
94
|
+
await agent.send(prompt);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const envelope: TheokitErrorEnvelope = toEnvelope(err); // { code, message, meta?, ext? }
|
|
97
|
+
send(envelope); // code is a TheokitErrorCode, e.g. "RATE_LIMITED"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Inbound edge (e.g. worker receiving the envelope):
|
|
101
|
+
const restored = fromEnvelope(envelope); // a TheokitAgentError subclass
|
|
102
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*client*"
|
|
5
|
+
- "**/*Client*"
|
|
6
|
+
description: TheoKit SDK low-level HTTP client — TheoKitClient (DEPRECATED; prefer the Agent façade)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit Client
|
|
10
|
+
|
|
11
|
+
`TheoKitClient` is a browser-safe, zero-Node-dependency HTTP client (native
|
|
12
|
+
`fetch` + manual SSE parsing) for a legacy server-adapter contract.
|
|
13
|
+
|
|
14
|
+
> DEPRECATED since 2.x — the `@theokit/sdk/client` sub-path consumes a legacy
|
|
15
|
+
> server-adapter HTTP contract (`POST /agent/send`, `GET /agent/stream`) that the
|
|
16
|
+
> ecosystem no longer produces, and will be removed in the next major. For
|
|
17
|
+
> in-process runs use the `Agent` façade (`@theokit/sdk`); for HTTP, use the
|
|
18
|
+
> framework's typed `POST /api/agents/<name>` client. Reach for this only when
|
|
19
|
+
> maintaining an existing integration against the old contract.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { TheoKitClient } from "@theokit/sdk/client";
|
|
23
|
+
import type { ClientOptions, SendResponse, StreamEvent } from "@theokit/sdk/client";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Construct
|
|
27
|
+
|
|
28
|
+
The constructor takes `ClientOptions` — `baseUrl` (required), optional `basePath`
|
|
29
|
+
and `headers`.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const client = new TheoKitClient({
|
|
33
|
+
baseUrl: "https://adapter.example.com",
|
|
34
|
+
basePath: "/agent", // optional
|
|
35
|
+
headers: { authorization: "Bearer …" }, // optional
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Send (one-shot)
|
|
40
|
+
|
|
41
|
+
`send(input)` POSTs and resolves a `SendResponse` (`{ status, output?, error? }`).
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const res: SendResponse = await client.send("summarize the repo");
|
|
45
|
+
if (res.error) throw new Error(res.error);
|
|
46
|
+
console.log(res.status, res.output);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Stream (SSE)
|
|
50
|
+
|
|
51
|
+
`stream(input)` returns an `AsyncGenerator<StreamEvent>`; each `StreamEvent` has a
|
|
52
|
+
`type` and an optional `text` (plus arbitrary extra fields).
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
for await (const event of client.stream("build the changelog")) {
|
|
56
|
+
if (event.type === "text" && event.text) process.stdout.write(event.text);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*compact*"
|
|
5
|
+
- "**/*Compact*"
|
|
6
|
+
description: TheoKit SDK compaction reference — compactTranscript, shouldCompact, checkpoints, context-overflow
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit Compaction
|
|
10
|
+
|
|
11
|
+
Public context-management helpers. Every function is pure and never mutates its
|
|
12
|
+
input. A `CompressibleMessage` is `{ role: "user" | "assistant" | "system"; content: string }`.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import {
|
|
16
|
+
compactTranscript,
|
|
17
|
+
shouldCompact,
|
|
18
|
+
estimateTokens,
|
|
19
|
+
buildCheckpoint,
|
|
20
|
+
filterFromLatestCheckpoint,
|
|
21
|
+
isContextOverflowError,
|
|
22
|
+
CHECKPOINT_MARKER,
|
|
23
|
+
SUMMARY_TEMPLATE,
|
|
24
|
+
type CompactTranscriptOptions,
|
|
25
|
+
type ShouldCompactInput,
|
|
26
|
+
type CompressibleMessage,
|
|
27
|
+
} from "@theokit/sdk/compaction";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Pre-call gate — `estimateTokens` + `shouldCompact`
|
|
31
|
+
|
|
32
|
+
`estimateTokens` is a tokenizer-free `ceil(text.length / 4)` heuristic — a cheap
|
|
33
|
+
gate, NOT exact tokenization. `shouldCompact` is pure: the caller supplies the
|
|
34
|
+
model's window.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const estimated = estimateTokens(transcript.map((m) => m.content).join("\n"));
|
|
38
|
+
|
|
39
|
+
const input: ShouldCompactInput = {
|
|
40
|
+
estimated,
|
|
41
|
+
contextWindow: 200_000,
|
|
42
|
+
buffer: 8_000, // headroom to reserve (output + safety margin)
|
|
43
|
+
maxOutput: 4_000, // optional; separate response reservation (default 0)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (shouldCompact(input)) {
|
|
47
|
+
// compact before sending — see below
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## `compactTranscript` — summarize the older window
|
|
52
|
+
|
|
53
|
+
Default `keepRecent` mode keeps the last N turns verbatim (default 6) and
|
|
54
|
+
preserves leading system prompts. The older window is summarized via the
|
|
55
|
+
caller-supplied `summarize` callback (or dropped if omitted). With `failSafe`, a
|
|
56
|
+
thrown summarizer returns the ORIGINAL transcript instead of propagating.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const opts: CompactTranscriptOptions = {
|
|
60
|
+
keepRecent: 6, // OR keepTokens: 40_000 (token-budget mode, takes precedence)
|
|
61
|
+
failSafe: true,
|
|
62
|
+
summarize: async (older: CompressibleMessage[], template: string) => {
|
|
63
|
+
// template is SUMMARY_TEMPLATE unless overridden via summaryTemplate
|
|
64
|
+
const summary = await callYourModel(template, older);
|
|
65
|
+
return { role: "system", content: summary };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const compacted = await compactTranscript(transcript, opts);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Checkpoints — mark and filter
|
|
73
|
+
|
|
74
|
+
`buildCheckpoint` produces a `system` turn whose content starts with
|
|
75
|
+
`CHECKPOINT_MARKER`. `filterFromLatestCheckpoint` returns turns relative to the
|
|
76
|
+
most recent marker (`include: "after"` excludes it — the default; `"from"`
|
|
77
|
+
includes it).
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const marked = [...transcript, buildCheckpoint("milestone: tests green")];
|
|
81
|
+
|
|
82
|
+
const recent = filterFromLatestCheckpoint(marked); // after (exclusive)
|
|
83
|
+
const withHead = filterFromLatestCheckpoint(marked, { include: "from" });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Context-overflow detection
|
|
87
|
+
|
|
88
|
+
`isContextOverflowError` is `true` only for a `TheokitAgentError` reporting the
|
|
89
|
+
typed `context_too_long` code — never a brittle message regex.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
try {
|
|
93
|
+
await agent.send(prompt);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (isContextOverflowError(err)) {
|
|
96
|
+
const compacted = await compactTranscript(transcript, { keepRecent: 4 });
|
|
97
|
+
// retry with the compacted transcript
|
|
98
|
+
} else {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
description: Bound in-process parallelism with Semaphore.create and mapWithConcurrency from @theokit/sdk/concurrency.
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*concurren*"
|
|
6
|
+
- "**/*semaphore*"
|
|
7
|
+
- "**/*Semaphore*"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TheoKit SDK -- Concurrency
|
|
11
|
+
|
|
12
|
+
In-house concurrency helpers (no `p-limit`/`p-map` dependency). `Semaphore.create(permits)` builds an N-permit async counting gate; `mapWithConcurrency` runs an async mapper over items with bounded parallelism while preserving input order.
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Semaphore, mapWithConcurrency } from "@theokit/sdk/concurrency";
|
|
18
|
+
import type { AsyncSemaphore } from "@theokit/sdk/concurrency";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Signatures
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
class Semaphore {
|
|
25
|
+
static create(permits: number): AsyncSemaphore; // canonical factory (ADR 0015)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AsyncSemaphore {
|
|
29
|
+
acquire(): Promise<() => void>; // returns a release fn; call it exactly once
|
|
30
|
+
inFlight(): number; // permits currently held
|
|
31
|
+
pending(): number; // in-flight + queued waiters
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mapWithConcurrency<T, R>(
|
|
35
|
+
items: ReadonlyArray<T>,
|
|
36
|
+
concurrency: number, // positive integer; validated
|
|
37
|
+
fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
38
|
+
options?: { signal?: AbortSignal },
|
|
39
|
+
): Promise<R[]>; // ordered; fail-fast; throws ConfigurationError on bad concurrency
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Semaphore -- release in a finally
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const sem = Semaphore.create(4); // at most 4 in flight
|
|
46
|
+
|
|
47
|
+
async function guarded<T>(task: () => Promise<T>): Promise<T> {
|
|
48
|
+
const release = await sem.acquire();
|
|
49
|
+
try {
|
|
50
|
+
return await task();
|
|
51
|
+
} finally {
|
|
52
|
+
release(); // release exactly once (idempotent, but leaking it consumes a permit)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## mapWithConcurrency -- ordered bounded map
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const results = await mapWithConcurrency(
|
|
62
|
+
["a", "b", "c"],
|
|
63
|
+
2, // max 2 concurrent fetches
|
|
64
|
+
async (url, _index, signal) => (await fetch(url, { signal })).json(),
|
|
65
|
+
{ signal: controller.signal },
|
|
66
|
+
);
|
|
67
|
+
// results align with input order; rejects on the first task error
|
|
68
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*filesystem*"
|
|
5
|
+
- "**/*Filesystem*"
|
|
6
|
+
description: TheoKit SDK filesystem seam — FilesystemBackend, LocalFilesystem, boundary-enforced storage with read-before-write safety
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit Filesystem
|
|
10
|
+
|
|
11
|
+
A pluggable, boundary-enforced file *storage* provider for agent file tools —
|
|
12
|
+
the storage-side twin of the sandbox seam. Ship a `FilesystemBackend` (default
|
|
13
|
+
`LocalFilesystem`) with an optional `readOnly` flag and a per-request resolver.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
LocalFilesystem, FilesystemBackend, resolveFilesystem,
|
|
18
|
+
} from "@theokit/sdk/filesystem";
|
|
19
|
+
import type {
|
|
20
|
+
FilesystemProvider, FilesystemConfig, FileStat, WriteFileOptions,
|
|
21
|
+
} from "@theokit/sdk/filesystem";
|
|
22
|
+
import {
|
|
23
|
+
FileNotFoundError, FilesystemError,
|
|
24
|
+
FilesystemReadOnlyError, FilesystemSecurityError, StaleFileError,
|
|
25
|
+
} from "@theokit/sdk/filesystem";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## LocalFilesystem
|
|
29
|
+
|
|
30
|
+
Boundary-enforced over `node:fs/promises`. Every path resolves within `basePath`;
|
|
31
|
+
traversal / symlink escapes are rejected with `FilesystemSecurityError`. NOT an
|
|
32
|
+
isolation boundary — run untrusted code inside a container/VM.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const fs = new LocalFilesystem({ basePath: "/workspace", readOnly: false });
|
|
36
|
+
|
|
37
|
+
const stat: FileStat = await fs.writeFile("notes.md", "# hello"); // returns FileStat
|
|
38
|
+
const text = await fs.readFile("notes.md");
|
|
39
|
+
const names = await fs.list("."); // entry names, not recursive
|
|
40
|
+
if (await fs.exists("notes.md")) { /* … */ }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Read-before-write safety (SE32)
|
|
44
|
+
|
|
45
|
+
`stat().mtimeMs` is the oracle. Pass `expectedMtime` so a concurrent change makes
|
|
46
|
+
the write fail with `StaleFileError` instead of silently clobbering.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const before = await fs.stat("notes.md");
|
|
50
|
+
try {
|
|
51
|
+
await fs.writeFile("notes.md", updated, { expectedMtime: before.mtimeMs });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err instanceof StaleFileError) { /* someone changed it — re-read + merge */ }
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Per-request provider (multi-tenant)
|
|
58
|
+
|
|
59
|
+
A `FilesystemProvider` is a backend OR a resolver `(ctx) => backend` run at
|
|
60
|
+
tool-execution time, giving each request its own root. `resolveFilesystem`
|
|
61
|
+
collapses either form to a concrete backend.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const provider: FilesystemProvider<{ tenant: string }> = (ctx) =>
|
|
65
|
+
new LocalFilesystem({ basePath: `/data/${ctx.tenant}` });
|
|
66
|
+
|
|
67
|
+
const backend = await resolveFilesystem(provider, { tenant: "acme" });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Custom backend
|
|
71
|
+
|
|
72
|
+
Extend `FilesystemBackend` and implement `readFile` / `writeFile` / `stat` /
|
|
73
|
+
`list`; `exists()`, `readOnly`, and `basePath` derive on the base class. Map raw
|
|
74
|
+
Node errors to the typed errors above (`FileNotFoundError`, `FilesystemError`).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*message*"
|
|
5
|
+
- "**/*Message*"
|
|
6
|
+
description: TheoKit SDK message readers — assistantText, extractToolUses, costAmountUsd over the SDKMessage stream
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit Messages
|
|
10
|
+
|
|
11
|
+
Pure readers over the `SDKMessage` stream — no I/O, no mutation, deterministic.
|
|
12
|
+
Use these instead of re-implementing a wire-event mapper.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { assistantText, extractToolUses, costAmountUsd } from "@theokit/sdk/messages";
|
|
16
|
+
import type { SDKMessage, ToolUseBlock } from "@theokit/sdk";
|
|
17
|
+
import type { CostBreakdown } from "@theokit/sdk";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## `assistantText(msg)` — concatenate assistant text blocks
|
|
21
|
+
|
|
22
|
+
Returns `""` for any non-assistant message (or an assistant with no text
|
|
23
|
+
blocks). `tool_use` blocks are ignored — only `text` blocks contribute.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
for await (const event of run.stream()) {
|
|
27
|
+
// event is an SDKMessage; assistantText is safe on any variant
|
|
28
|
+
const text = assistantText(event);
|
|
29
|
+
if (text) process.stdout.write(text);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## `extractToolUses(msg)` — read assistant `ToolUseBlock`s
|
|
34
|
+
|
|
35
|
+
Returns `[]` for any non-assistant message. This reads the assistant message's
|
|
36
|
+
content blocks — NOT the separate `tool_call` lifecycle event (a different
|
|
37
|
+
stream). Tool `input` is `unknown`; parse it defensively.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const uses: ToolUseBlock[] = extractToolUses(event);
|
|
41
|
+
for (const use of uses) {
|
|
42
|
+
console.log(use.name, use.id); // use.input is `unknown` — validate before use
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## `costAmountUsd(cost)` — honesty-preserving cost read
|
|
47
|
+
|
|
48
|
+
Returns `number | undefined`. `undefined` means "cost unknown" — distinct from a
|
|
49
|
+
real `$0` (e.g. a subscription-included route). NEVER coerced to 0.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const amount = costAmountUsd(cost); // cost: CostBreakdown | undefined
|
|
53
|
+
if (amount === undefined) {
|
|
54
|
+
console.log("cost unknown");
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`$${amount.toFixed(4)}`);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*model*"
|
|
5
|
+
- "**/*Model*"
|
|
6
|
+
description: TheoKit SDK model helpers — @theokit/sdk/models (parseModelId, resolveModelCapabilities, toModelOption, humanizeModelName); API catalog via Theokit.models.list()
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit Models
|
|
10
|
+
|
|
11
|
+
Pure, sync, offline helpers for model ids. No network — these read a static catalog
|
|
12
|
+
and parse strings. For the live API-backed catalog, use `Theokit.models.list()` from
|
|
13
|
+
the main `@theokit/sdk` barrel.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
parseModelId,
|
|
18
|
+
resolveModelCapabilities,
|
|
19
|
+
toModelOption,
|
|
20
|
+
humanizeModelName,
|
|
21
|
+
type ModelCapabilities,
|
|
22
|
+
type ModelOption,
|
|
23
|
+
type ParsedModelId,
|
|
24
|
+
} from "@theokit/sdk/models";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Parse a model id
|
|
28
|
+
|
|
29
|
+
`parseModelId` splits on the first `/` into `{ provider, name }`. No `/` means
|
|
30
|
+
`provider` is `undefined` (so callers can fall back to env-var detection). Tag
|
|
31
|
+
suffixes like `:3b` / `:latest` stay part of `name`.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const a: ParsedModelId = parseModelId("anthropic/claude-3-5-sonnet");
|
|
35
|
+
// { provider: "anthropic", name: "claude-3-5-sonnet" }
|
|
36
|
+
|
|
37
|
+
const b = parseModelId("openrouter/meta-llama/llama-3");
|
|
38
|
+
// { provider: "openrouter", name: "meta-llama/llama-3" } (embedded slash kept)
|
|
39
|
+
|
|
40
|
+
const c = parseModelId("claude-sonnet-4-6");
|
|
41
|
+
// { provider: undefined, name: "claude-sonnet-4-6" }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Gate features by capability
|
|
45
|
+
|
|
46
|
+
`resolveModelCapabilities` returns typed flags + token limits from an OFFLINE
|
|
47
|
+
catalog. It strips routing prefixes (`openrouter/`/`vertex/`/`bedrock/`) and the
|
|
48
|
+
OpenRouter `:variant` suffix before lookup; unknown models get conservative defaults.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const caps: ModelCapabilities = resolveModelCapabilities("anthropic/claude-3-5-sonnet");
|
|
52
|
+
// { supportsVision, supportsStructuredOutput, supportsToolUse,
|
|
53
|
+
// supportsCacheControl, maxContextTokens, maxOutputTokens }
|
|
54
|
+
|
|
55
|
+
if (!caps.supportsVision) {
|
|
56
|
+
throw new Error("This model cannot accept images");
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Build dropdown options
|
|
61
|
+
|
|
62
|
+
`humanizeModelName` produces a best-effort label; `toModelOption` composes it with
|
|
63
|
+
`parseModelId` into `{ value, label, provider }`.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
humanizeModelName("anthropic/claude-3-5-sonnet"); // "Claude 3 5 Sonnet"
|
|
67
|
+
|
|
68
|
+
const opt: ModelOption = toModelOption("openrouter/meta-llama/llama-3");
|
|
69
|
+
// { value: "openrouter/meta-llama/llama-3", label: "...", provider: "openrouter" }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Live catalog (API-backed)
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { Theokit } from "@theokit/sdk";
|
|
76
|
+
|
|
77
|
+
const models = await Theokit.models.list({ apiKey: process.env.THEOKIT_API_KEY });
|
|
78
|
+
const options: ModelOption[] = models.map((m) => toModelOption(m.id));
|
|
79
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
description: Path-traversal, symlink-escape, and forbidden-path guards from @theokit/sdk/path-safety for agent file I/O.
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*path-safety*"
|
|
6
|
+
- "**/*pathsafety*"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit SDK -- Path Safety
|
|
10
|
+
|
|
11
|
+
TOCTOU-safe path primitives to wire wherever user input becomes a filesystem path in a custom tool. `safePathJoin` resolves then prefix-checks; `assertNoSymlinkEscape` resolves the whole symlink chain via `realpathSync`; `isForbiddenPath` blocks sensitive files even when they are lexically inside the project.
|
|
12
|
+
|
|
13
|
+
## Import
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
safePathJoin,
|
|
18
|
+
assertNoSymlinkEscape,
|
|
19
|
+
isForbiddenPath,
|
|
20
|
+
safeFilenameForId,
|
|
21
|
+
sanitizeIdentifier,
|
|
22
|
+
PathTraversalError,
|
|
23
|
+
ForbiddenPathError,
|
|
24
|
+
} from "@theokit/sdk/path-safety";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Signatures
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
function safePathJoin(base: string, ...parts: string[]): string; // throws PathTraversalError on escape
|
|
31
|
+
function assertNoSymlinkEscape(path: string, base: string): void; // throws PathTraversalError on symlink escape
|
|
32
|
+
function isForbiddenPath(input: string): boolean; // true for .env*, .git/, node_modules/, .theo/, lock files
|
|
33
|
+
function safeFilenameForId(id: string, options?: { maxLen?: number }): string; // total: passthrough or h-<16hex>
|
|
34
|
+
function sanitizeIdentifier(input: string, options?: { maxLen?: number }): string; // grammar ^[a-z0-9][a-z0-9-_]*$
|
|
35
|
+
|
|
36
|
+
class PathTraversalError extends ConfigurationError { constructor(input: string, resolvedPath: string); } // code "path_traversal"
|
|
37
|
+
class ForbiddenPathError extends ConfigurationError { constructor(path: string); } // code "forbidden_path"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Guard a tool's file read
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const projectRoot = process.cwd();
|
|
44
|
+
|
|
45
|
+
function resolveUserPath(userPath: string): string {
|
|
46
|
+
if (isForbiddenPath(userPath)) throw new ForbiddenPathError(userPath);
|
|
47
|
+
const safe = safePathJoin(projectRoot, userPath); // throws PathTraversalError on "../" escape
|
|
48
|
+
assertNoSymlinkEscape(safe, projectRoot); // throws PathTraversalError on symlink escape
|
|
49
|
+
return safe;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Derive a safe filename from an opaque id
|
|
54
|
+
|
|
55
|
+
`safeFilenameForId` never throws on a non-empty string: it returns the id verbatim when it already matches the safe grammar, otherwise a deterministic `h-<16 hex>` sha256 token.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
safeFilenameForId("550e8400-e29b-41d4-a716-446655440000"); // passthrough
|
|
59
|
+
safeFilenameForId("user@example.com"); // "h-<16hex>"
|
|
60
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*persist*"
|
|
5
|
+
- "**/*Persist*"
|
|
6
|
+
- "**/*.jsonl"
|
|
7
|
+
- "**/*sqlite*"
|
|
8
|
+
description: TheoKit SDK persistence primitives — atomic writes, JSONL persist/resume, resilient SQLite, cross-process locks
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# TheoKit Persistence
|
|
12
|
+
|
|
13
|
+
Durable, crash-safe persistence helpers on the STABLE `@theokit/sdk/persistence`
|
|
14
|
+
sub-path. Do NOT import them from `@theokit/sdk/internal/persistence` (semver-exempt).
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import {
|
|
18
|
+
// atomic writes
|
|
19
|
+
replaceFileAtomic, atomicWriteText, atomicWriteJson,
|
|
20
|
+
// jsonl persist/resume
|
|
21
|
+
appendJsonl, loadJsonl, readJsonlIds, JsonlParseError,
|
|
22
|
+
// resilient sqlite
|
|
23
|
+
openSqliteResilient, applyWalWithFallback, isCorruptionError, sanitizeFts5Query,
|
|
24
|
+
// locks
|
|
25
|
+
withFileLock, withCwdMutex,
|
|
26
|
+
// misc
|
|
27
|
+
PersistenceSchema, transcriptPath, encodeProjectDir,
|
|
28
|
+
} from "@theokit/sdk/persistence";
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Atomic writes
|
|
32
|
+
|
|
33
|
+
Crash mid-write leaves either the old file intact or the new file complete —
|
|
34
|
+
never a half-written file (temp + fsync + rename). `atomicWriteJson` /
|
|
35
|
+
`atomicWriteText` auto-`mkdir` the parent dir; `replaceFileAtomic` does not.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
await replaceFileAtomic("/data/config.md", markdown); // (filePath, content)
|
|
39
|
+
await atomicWriteText("/data/notes.txt", text); // + auto-mkdir
|
|
40
|
+
await atomicWriteJson("/data/state.json", { runs: 3 }); // default indent 2, trailing \n
|
|
41
|
+
await atomicWriteJson("/data/state.json", data, { indent: 0, trailingNewline: false });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## JSONL persist / resume
|
|
45
|
+
|
|
46
|
+
`appendJsonl` writes one `\n`-terminated line per record (synchronous, interleave-safe
|
|
47
|
+
within a process). Resume a crashed batch by reading already-persisted keys.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
appendJsonl("/data/results.jsonl", { id: "task-1", status: "passed" });
|
|
51
|
+
|
|
52
|
+
// Skip rows already done (partial trailing line tolerated; missing file -> empty set)
|
|
53
|
+
const done = readJsonlIds("/data/results.jsonl", (row) =>
|
|
54
|
+
row.status === "passed" ? String(row.id) : undefined,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// loadJsonl throws JsonlParseError (with 1-based .line) on a malformed line
|
|
58
|
+
const rows = loadJsonl<{ id: string }>("/data/results.jsonl");
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Resilient SQLite
|
|
62
|
+
|
|
63
|
+
`openSqliteResilient` loads the driver, applies WAL (with DELETE fallback), and
|
|
64
|
+
recovers from corruption by renaming the file aside. Apply schema in `onOpen`.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const db = await openSqliteResilient({
|
|
68
|
+
filePath: "/data/index.db",
|
|
69
|
+
label: "memory-index",
|
|
70
|
+
onOpen: (db) => db.exec("CREATE TABLE IF NOT EXISTS docs (id TEXT PRIMARY KEY)"),
|
|
71
|
+
});
|
|
72
|
+
applyWalWithFallback(db, "memory-index"); // { mode: "wal"|"delete", fellBack }
|
|
73
|
+
const q = sanitizeFts5Query("error-code"); // "" means caller must short-circuit
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Locks
|
|
77
|
+
|
|
78
|
+
`withFileLock` takes a cross-process lock (needs the `proper-lockfile` peer dep;
|
|
79
|
+
falls back to in-process `withCwdMutex` with a warning). `withCwdMutex` serializes
|
|
80
|
+
by key within one process.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await withFileLock("/data/state.json", async () => { /* exclusive section */ });
|
|
84
|
+
await withCwdMutex("migrate-config", async () => { /* in-process critical section */ });
|
|
85
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*project*"
|
|
5
|
+
- "**/*Project*"
|
|
6
|
+
- "**/THEO.md"
|
|
7
|
+
description: TheoKit SDK project-instruction reader/writer — hierarchical walk-up discovery + atomic write of THEO.md
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TheoKit Project Instructions
|
|
11
|
+
|
|
12
|
+
Hierarchical project-instruction reader/writer on `@theokit/sdk/project`. The
|
|
13
|
+
reader walks up from `cwd` collecting an instruction file (default `THEO.md`) and
|
|
14
|
+
NEVER throws; the writer writes it atomically and FAILS LOUD on error.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import {
|
|
18
|
+
readProjectInstructions, writeProjectInstructions,
|
|
19
|
+
} from "@theokit/sdk/project";
|
|
20
|
+
import type {
|
|
21
|
+
ProjectInstructions, ProjectInstructionFile, ProjectInstructionScope,
|
|
22
|
+
ReadProjectInstructionsOptions, WriteProjectInstructionsOptions,
|
|
23
|
+
} from "@theokit/sdk/project";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Read (walk-up, never throws)
|
|
27
|
+
|
|
28
|
+
Discovers every `<dir>/<filename>` from `cwd` up to the filesystem root (or
|
|
29
|
+
`stopDir`). Returns `files` nearest-first plus a `content` reduction chosen by
|
|
30
|
+
`scope` (`"nearest"` → innermost file; `"merged"` → all joined root-first).
|
|
31
|
+
No file found → `{ files: [], content: undefined }`.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const result: ProjectInstructions = await readProjectInstructions(process.cwd(), {
|
|
35
|
+
filename: "THEO.md", // default "THEO.md"
|
|
36
|
+
scope: "merged", // default "nearest"
|
|
37
|
+
stopDir: "/home/me/repo",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (result.content) applySystemPrompt(result.content);
|
|
41
|
+
for (const f of result.files) console.log(f.path, f.content.length);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Write (atomic, fails loud)
|
|
45
|
+
|
|
46
|
+
Writes `<cwd>/<filename>` via temp + fsync + rename. An unsafe `filename`
|
|
47
|
+
(traversal, separators, absolute) is rejected with `ConfigurationError`
|
|
48
|
+
(`code: "unsafe_filename"`); a real write error (e.g. missing parent dir)
|
|
49
|
+
propagates — never silently swallowed.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await writeProjectInstructions(process.cwd(), "# Project rules\n…", {
|
|
53
|
+
filename: "THEO.md", // default "THEO.md"
|
|
54
|
+
});
|
|
55
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*retry*"
|
|
5
|
+
- "**/*Retry*"
|
|
6
|
+
description: TheoKit SDK retry primitive — @theokit/sdk/retry (Retry.create executor with exponential backoff + full jitter, RetryOptions)
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit Retry
|
|
10
|
+
|
|
11
|
+
`@theokit/sdk/retry` exposes one primitive: `Retry`. NOTE — `Retry.create` is an
|
|
12
|
+
EXECUTOR, not a constructor. `Retry.create(fn, opts)` RUNS `fn` with retry/backoff and
|
|
13
|
+
resolves to its result (`Promise<T>`) — it does not return a `Retry` instance.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Retry, type RetryOptions } from "@theokit/sdk/retry";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Run a fn with retry
|
|
20
|
+
|
|
21
|
+
Exponential backoff with full jitter. The default `isRetryable` predicate is the SDK's
|
|
22
|
+
own `isTransientError`, so retries follow the SDK's error classification (rate-limit /
|
|
23
|
+
network retry; business-rule violations do not).
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const data = await Retry.create(() => fetchJson(url));
|
|
27
|
+
// retries transient failures up to 3 times (4 attempts total), then throws the last error
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Tuning with `RetryOptions`
|
|
31
|
+
|
|
32
|
+
All fields are optional; defaults shown in comments.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const result = await Retry.create(
|
|
36
|
+
() => callFlakyApi(),
|
|
37
|
+
{
|
|
38
|
+
retries: 5, // retries AFTER the first attempt (default 3)
|
|
39
|
+
initialDelayMs: 200, // base backoff for the first retry (default 100)
|
|
40
|
+
maxDelayMs: 10_000, // cap per single sleep (default 30_000)
|
|
41
|
+
backoffMultiplier: 2, // exponential multiplier per retry (default 2)
|
|
42
|
+
isRetryable: (err) => err instanceof NetworkGlitch, // default: isTransientError
|
|
43
|
+
signal: controller.signal, // aborts the backoff loop when triggered
|
|
44
|
+
} satisfies RetryOptions,
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`rng` and `sleep` are also injectable — override them for deterministic tests (no real
|
|
49
|
+
timers). When retries are exhausted or the error is not retryable, `Retry.create`
|
|
50
|
+
re-throws the last error.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*sandbox*"
|
|
5
|
+
- "**/*Sandbox*"
|
|
6
|
+
description: TheoKit SDK sandbox reference — SandboxBackend / LocalSandbox execution + provisionRepo
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit Sandbox
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import {
|
|
13
|
+
LocalSandbox,
|
|
14
|
+
provisionRepo,
|
|
15
|
+
SandboxBackend,
|
|
16
|
+
SandboxNotAvailableError,
|
|
17
|
+
SandboxSecurityError,
|
|
18
|
+
RepoProvisionError,
|
|
19
|
+
type ExecuteResult,
|
|
20
|
+
type SandboxConfig,
|
|
21
|
+
type ProvisionRepoOptions,
|
|
22
|
+
} from "@theokit/sdk/sandbox";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## `LocalSandbox` — subprocess execution
|
|
26
|
+
|
|
27
|
+
`LocalSandbox` runs commands via `/bin/sh -c` in the SAME OS as the host. It is
|
|
28
|
+
**NOT an isolation boundary** — no process, filesystem, or network isolation.
|
|
29
|
+
Its only safety affordances are a wall-clock timeout, an output-size cap, and
|
|
30
|
+
env scrubbing. For real isolation of untrusted code, use a container/VM backend.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const sandbox = new LocalSandbox({
|
|
34
|
+
workDir: "/tmp/work",
|
|
35
|
+
timeoutMs: 5_000,
|
|
36
|
+
maxOutputBytes: 1024,
|
|
37
|
+
// env defaults to "inherit-scrubbed": drops *KEY* / *SECRET* / *TOKEN* /
|
|
38
|
+
// *PASSWORD* / *_AUTH* from the child. Pass "all" or "core" to change.
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const result: ExecuteResult = await sandbox.execute("echo hi", { timeoutMs: 2_000 });
|
|
42
|
+
console.log(result.stdout, result.exitCode, result.timedOut);
|
|
43
|
+
|
|
44
|
+
// Derived helpers on the base class (no need to reimplement):
|
|
45
|
+
await sandbox.writeFile("a.txt", "content");
|
|
46
|
+
const text = await sandbox.readFile("a.txt");
|
|
47
|
+
const files = await sandbox.glob("**/*.ts");
|
|
48
|
+
const hits = await sandbox.grep("TODO", "src");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Custom backend — implement only 2 abstract methods
|
|
52
|
+
|
|
53
|
+
New backends (Docker, Firecracker, E2B) extend `SandboxBackend` and implement
|
|
54
|
+
only `execute` + `uploadFile`; `readFile` / `writeFile` / `glob` / `grep` /
|
|
55
|
+
`listDir` are derived on the base class.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
class MyBackend extends SandboxBackend {
|
|
59
|
+
async execute(command: string, opts?: { timeoutMs?: number }): Promise<ExecuteResult> {
|
|
60
|
+
// run command remotely; return { stdout, stderr, exitCode, timedOut }
|
|
61
|
+
throw new SandboxNotAvailableError("backend offline");
|
|
62
|
+
}
|
|
63
|
+
async uploadFile(path: string, content: string | Buffer): Promise<void> {
|
|
64
|
+
/* ... */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`SandboxSecurityError` (`code: "sandbox_security"`) and
|
|
70
|
+
`SandboxNotAvailableError` (`code: "sandbox_not_available"`) both extend `Error`.
|
|
71
|
+
|
|
72
|
+
## `provisionRepo` — clone + checkout into the sandbox
|
|
73
|
+
|
|
74
|
+
Clones `repoUrl` into `<workDir>/<instanceId>` and checks out `ref`, running
|
|
75
|
+
every git command through the sandbox. The `sandbox` argument is optional — when
|
|
76
|
+
omitted a default `LocalSandbox` (process cwd) is used.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const opts: ProvisionRepoOptions = {
|
|
80
|
+
repoUrl: "https://github.com/acme/repo.git",
|
|
81
|
+
ref: "main", // branch, tag, or SHA (rejected if it begins with "-")
|
|
82
|
+
instanceId: "task-001", // validated to [A-Za-z0-9._-] — becomes a dir name
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const { repoDir } = await provisionRepo(new LocalSandbox({ workDir: "/tmp/work" }), opts);
|
|
86
|
+
// or, with the default LocalSandbox: await provisionRepo(opts);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await provisionRepo({ repoUrl: "bad", ref: "main", instanceId: "x" });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof RepoProvisionError) console.error(err.instanceId, err.message);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
description: Clean model-emitted tool arguments with sanitizeToolInput from @theokit/sdk/sanitize (total, never throws).
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*sanitize*"
|
|
6
|
+
- "**/*Sanitize*"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit SDK -- Sanitize
|
|
10
|
+
|
|
11
|
+
`sanitizeToolInput` cleans the raw arguments a model emitted for a tool call: trim (default on), optionally coerce string values toward their expected type, optionally repair malformed JSON. It is pure, synchronous, and TOTAL -- it NEVER throws (non-object input is returned unchanged) and never changes a value's meaning, only its hygiene/representation.
|
|
12
|
+
|
|
13
|
+
## Import
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { sanitizeToolInput } from "@theokit/sdk/sanitize";
|
|
17
|
+
import type { SanitizeOptions, SanitizeResult } from "@theokit/sdk/sanitize";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Signature
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
function sanitizeToolInput(
|
|
24
|
+
input: Record<string, unknown>,
|
|
25
|
+
options?: SanitizeOptions,
|
|
26
|
+
): SanitizeResult;
|
|
27
|
+
|
|
28
|
+
interface SanitizeOptions {
|
|
29
|
+
trim?: boolean; // trim whitespace on string values. Default true
|
|
30
|
+
coerce?: boolean; // "5"->5, "true"->true, "null"->null, JSON. Default false
|
|
31
|
+
repairJson?: boolean; // repair-then-parse malformed JSON strings (jsonrepair). Default false
|
|
32
|
+
schema?: ZodType; // when a z.object(...), coercion is schema-aware per top-level field
|
|
33
|
+
deep?: boolean; // recurse into nested objects/arrays. Default false (shallow)
|
|
34
|
+
maxDepth?: number; // max recursion depth when deep. Default 8
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SanitizeResult<T = Record<string, unknown>> {
|
|
38
|
+
value: T; // the sanitized copy
|
|
39
|
+
changed: boolean; // true when any value was altered
|
|
40
|
+
notes: string[]; // one human-readable line per change (for logging)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Trim only (default)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const { value, changed, notes } = sanitizeToolInput({ query: " hello " });
|
|
48
|
+
// value -> { query: "hello" }, changed -> true, notes -> ["query: trimmed whitespace"]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Schema-aware coercion inside a tool handler
|
|
52
|
+
|
|
53
|
+
Pass the tool's Zod object schema so a `z.string()` field keeps `"5"` as a string while a `z.number()` field coerces it. Never throws, so it is safe to run before `schema.parse`.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { z } from "zod";
|
|
57
|
+
|
|
58
|
+
const inputSchema = z.object({ count: z.number(), label: z.string() });
|
|
59
|
+
|
|
60
|
+
const { value } = sanitizeToolInput(
|
|
61
|
+
{ count: "5", label: "42" },
|
|
62
|
+
{ coerce: true, schema: inputSchema },
|
|
63
|
+
);
|
|
64
|
+
// value -> { count: 5, label: "42" } (label stays a string; count coerced)
|
|
65
|
+
const parsed = inputSchema.parse(value);
|
|
66
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
description: Discover SKILL.md packs and render the <skills> block with @theokit/sdk/skills, plus enabling skills on an agent.
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*skill*"
|
|
6
|
+
- "**/*Skill*"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# TheoKit SDK -- Skills
|
|
10
|
+
|
|
11
|
+
`discoverSkills` walks a directory for `<dir>/<name>/SKILL.md` packs, parses strict YAML frontmatter (`name`/`description` required; `category`/`dependencies` optional), skips malformed skills and symlink escapes, and NEVER throws (a missing/unreadable/non-directory path yields `[]`). `buildSkillsBlock` renders the prompt-injection-safe `<skills>` system-prompt block from the discovered list. These are the same primitives the SDK runtime uses internally for `.theokit/skills` discovery.
|
|
12
|
+
|
|
13
|
+
## Import
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { discoverSkills, buildSkillsBlock } from "@theokit/sdk/skills";
|
|
17
|
+
import type { Skill, DiscoverSkillsOptions, InvalidSkillInfo } from "@theokit/sdk/skills";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Signatures
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
function discoverSkills(dir: string, options?: DiscoverSkillsOptions): Promise<Skill[]>;
|
|
24
|
+
function buildSkillsBlock(
|
|
25
|
+
skills: ReadonlyArray<{ name: string; description: string }>,
|
|
26
|
+
): string | undefined; // undefined for an empty list
|
|
27
|
+
|
|
28
|
+
interface Skill {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
source: string; // absolute path to the discovered SKILL.md
|
|
32
|
+
category?: string;
|
|
33
|
+
dependencies?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DiscoverSkillsOptions {
|
|
37
|
+
onInvalidSkill?: (info: InvalidSkillInfo) => void; // called per malformed SKILL.md
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Discover and render
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const skills = await discoverSkills(".theokit/skills", {
|
|
45
|
+
onInvalidSkill: (info: InvalidSkillInfo) =>
|
|
46
|
+
console.warn(`skipped ${info.name}: ${info.code} — ${info.message}`),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// readdir order is OS-dependent; sort for a stable block
|
|
50
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
51
|
+
|
|
52
|
+
const block = buildSkillsBlock(skills); // string | undefined
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Skill packs live at `.theokit/skills/<name>/SKILL.md`
|
|
56
|
+
|
|
57
|
+
Each pack is a directory with a `SKILL.md` whose frontmatter has `name` + `description`. Enable specific packs on an agent by name:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { Agent } from "@theokit/sdk";
|
|
61
|
+
|
|
62
|
+
const agent = await Agent.create({
|
|
63
|
+
apiKey: process.env.THEOKIT_API_KEY!,
|
|
64
|
+
model: { id: "google/gemini-2.0-flash-001" },
|
|
65
|
+
local: { cwd: process.cwd() },
|
|
66
|
+
skills: { enabled: ["research", "code-review"] },
|
|
67
|
+
});
|
|
68
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*subagent*"
|
|
5
|
+
- "**/*a2a*"
|
|
6
|
+
- "**/*delegat*"
|
|
7
|
+
description: TheoKit SDK subagents — @theokit/sdk/a2a (SubAgent.create, AgentMailbox, MessageBus, delegation hooks) and @theokit/sdk/subagents (subagentToolWhitelist, withSubagentToolScope)
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TheoKit SubAgents
|
|
11
|
+
|
|
12
|
+
Two subpaths cover delegation. `@theokit/sdk/a2a` builds a child agent invocable as
|
|
13
|
+
a tool plus the in-process message bus; `@theokit/sdk/subagents` scopes which tools a
|
|
14
|
+
subagent may call.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import {
|
|
18
|
+
SubAgent,
|
|
19
|
+
AgentMailbox,
|
|
20
|
+
MessageBus,
|
|
21
|
+
MaxDelegationDepthError,
|
|
22
|
+
type SubAgentSpec,
|
|
23
|
+
} from "@theokit/sdk/a2a";
|
|
24
|
+
import { subagentToolWhitelist, withSubagentToolScope } from "@theokit/sdk/subagents";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## `SubAgent.create` — delegation as a tool
|
|
28
|
+
|
|
29
|
+
`SubAgent.create(spec, parentDepth?)` returns a `CustomTool`. When the LLM invokes it,
|
|
30
|
+
a child agent runs the input as a message. Depth is tracked — exceeding
|
|
31
|
+
`maxDelegationDepth` throws `MaxDelegationDepthError`.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const researcher = SubAgent.create({
|
|
35
|
+
name: "researcher",
|
|
36
|
+
description: "Delegate research questions to a focused child agent",
|
|
37
|
+
instructions: "You research topics and return a concise summary.",
|
|
38
|
+
model: "google/gemini-2.0-flash-001",
|
|
39
|
+
tools: [], // CustomTool[] the child may use
|
|
40
|
+
maxDelegationDepth: 3,
|
|
41
|
+
onDelegationStart: (ctx) => {
|
|
42
|
+
if (ctx.iteration > 5) return { proceed: false, rejectionReason: "too many calls" };
|
|
43
|
+
return { proceed: true };
|
|
44
|
+
},
|
|
45
|
+
onDelegationComplete: (ctx) => {
|
|
46
|
+
if (ctx.result) return { feedback: "(reviewed)" };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const agent = await Agent.create({
|
|
51
|
+
apiKey: process.env.THEOKIT_API_KEY!,
|
|
52
|
+
model: { id: "google/gemini-2.0-flash-001" },
|
|
53
|
+
local: { cwd: process.cwd() },
|
|
54
|
+
tools: [researcher],
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`SubAgentSpec` also supports `messageFilter` (opt-in parent-context forwarding, off by
|
|
59
|
+
default so memory isolation stays the default) and `includeToolResults` (append the
|
|
60
|
+
child's tool results, otherwise text-only).
|
|
61
|
+
|
|
62
|
+
## Inline subagents on `Agent.create`
|
|
63
|
+
|
|
64
|
+
Simple cases need no `SubAgent.create` — declare them inline:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const agent = await Agent.create({
|
|
68
|
+
apiKey: process.env.THEOKIT_API_KEY!,
|
|
69
|
+
model: { id: "google/gemini-2.0-flash-001" },
|
|
70
|
+
local: { cwd: process.cwd() },
|
|
71
|
+
agents: {
|
|
72
|
+
reviewer: {
|
|
73
|
+
description: "Reviews code for bugs",
|
|
74
|
+
prompt: "You are a strict code reviewer.",
|
|
75
|
+
model: "google/gemini-2.0-flash-001",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tool scoping — `withSubagentToolScope`
|
|
82
|
+
|
|
83
|
+
`subagentToolWhitelist(definition)` derives the allowed tool-name `Set` (or `undefined`
|
|
84
|
+
when unscoped) from `definition.tools`. `withSubagentToolScope` runs a fn under that
|
|
85
|
+
whitelist so a `tools: ["read_file"]` subagent provably cannot call `write_file`.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const whitelist = subagentToolWhitelist({ tools: ["read_file"] }); // Set(["read_file"])
|
|
89
|
+
|
|
90
|
+
await withSubagentToolScope({ tools: ["read_file"] }, async () => {
|
|
91
|
+
// dispatch veto enforces the whitelist here
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Agent-to-agent messaging
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const bus = new MessageBus();
|
|
99
|
+
const alice = new AgentMailbox("alice", bus);
|
|
100
|
+
const bob = new AgentMailbox("bob", bus);
|
|
101
|
+
|
|
102
|
+
bob.onMessage(async (msg) => ({ type: "ack", payload: { ok: true } }));
|
|
103
|
+
|
|
104
|
+
await alice.send("bob", { type: "greet", payload: { text: "hi" } });
|
|
105
|
+
const reply = await alice.request("bob", { type: "ping", payload: null }, { timeoutMs: 1000 });
|
|
106
|
+
|
|
107
|
+
alice.dispose();
|
|
108
|
+
bob.dispose();
|
|
109
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
user-invocable: false
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*task-store*"
|
|
5
|
+
- "**/*taskstore*"
|
|
6
|
+
- "**/*TaskStore*"
|
|
7
|
+
description: TheoKit SDK task persistence — @theokit/sdk/task-store (TaskStore interface, InMemoryTaskStore, JsonFileTaskStore, getTaskStoreFor factory)
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TheoKit Task Store
|
|
11
|
+
|
|
12
|
+
`@theokit/sdk/task-store` is the storage layer behind the task registry. Pick
|
|
13
|
+
`InMemoryTaskStore` (transient, single-process default) or `JsonFileTaskStore` (one JSON
|
|
14
|
+
file per task under a dir; single-process invariant — v0.2 SQLite covers cross-process).
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import {
|
|
18
|
+
getTaskStoreFor,
|
|
19
|
+
InMemoryTaskStore,
|
|
20
|
+
JsonFileTaskStore,
|
|
21
|
+
type TaskStore,
|
|
22
|
+
} from "@theokit/sdk/task-store";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The `TaskStore` interface
|
|
26
|
+
|
|
27
|
+
All methods are async. `TaskHandle` / `TaskFilter` come from the main `@theokit/sdk`
|
|
28
|
+
barrel.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
interface TaskStore {
|
|
32
|
+
insert(handle: TaskHandle): Promise<void>;
|
|
33
|
+
update(id: string, mutate: (h: TaskHandle) => TaskHandle): Promise<TaskHandle | undefined>;
|
|
34
|
+
get(id: string): Promise<TaskHandle | undefined>;
|
|
35
|
+
list(filter: TaskFilter): Promise<TaskHandle[]>;
|
|
36
|
+
delete(id: string): Promise<boolean>;
|
|
37
|
+
evictTerminalOlderThan(epochMs: number): Promise<number>;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Factory — `getTaskStoreFor`
|
|
42
|
+
|
|
43
|
+
Discriminated on `backend`; the `json` backend auto-creates its dir (mkdir recursive).
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const memory: TaskStore = getTaskStoreFor({ backend: "memory" });
|
|
47
|
+
const onDisk: TaskStore = getTaskStoreFor({ backend: "json", dir: ".theokit/tasks" });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or construct directly:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const store = new JsonFileTaskStore(".theokit/tasks"); // constructor(dir: string)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Reading tasks
|
|
57
|
+
|
|
58
|
+
`list` returns at most `filter.limit ?? 100` handles; `JsonFileTaskStore` hard-caps
|
|
59
|
+
loaded entries at 256 — page larger timelines via `submittedBefore`.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import type { TaskFilter } from "@theokit/sdk";
|
|
63
|
+
|
|
64
|
+
const filter: TaskFilter = { state: ["running", "queued"], kind: "run", limit: 50 };
|
|
65
|
+
const running = await store.list(filter);
|
|
66
|
+
|
|
67
|
+
for (const h of running) {
|
|
68
|
+
console.log(h.id, h.state, h.submittedAt);
|
|
69
|
+
if (h.cancelRequested) console.log(" (cross-process cancel requested)");
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`cancelRequested` is set by the CLI's cross-process best-effort cancel (EC-7); the owning
|
|
74
|
+
process polls it at checkpoints. `evictTerminalOlderThan(epochMs)` removes terminal
|
|
75
|
+
handles older than a cutoff and returns the count removed.
|
package/package.json
CHANGED