@vincemakes/kiso-runtime 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kiso contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # @vincemakes/kiso-runtime
2
+
3
+ The durable session layer: createAgent, AgentSession, Run, the
4
+ crash-safe append-only JSONL store (torn-tail repair, cross-process
5
+ writer locks, expected-last-seq CAS), per-run recovery (session.resume),
6
+ real approval pauses, and the uncertain-execution ledger.
7
+
8
+ See the repository README for the framework overview.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * AgentRuntime + createAgent — the high-level entry point (Phase C).
3
+ *
4
+ * const agent = createAgent({ model, systemPrompt, tools, store,
5
+ * permissionPolicy, provider });
6
+ * const session = await agent.session({ id: "demo" });
7
+ * for await (const event of session.run("Inspect this repository")) { }
8
+ *
9
+ * The runtime is provider-agnostic: an adapter may be injected directly, or
10
+ * a provider name triggers a lazy import of the matching @kiso provider
11
+ * package (optional peers — an unused provider costs nothing). The kernel
12
+ * itself stays dependency-free; the SDKs live in the provider packages.
13
+ */
14
+ import { type Adapter, type HookHost, type Tool } from "@vincemakes/kiso-core";
15
+ import { AgentSession } from "./session.js";
16
+ import type { SessionStore } from "./store.js";
17
+ export interface PermissionRule {
18
+ readonly tool: string;
19
+ readonly action: "allow" | "deny" | "defer";
20
+ }
21
+ export interface PermissionPolicy {
22
+ /** First matching rule wins. */
23
+ readonly rules: readonly PermissionRule[];
24
+ /** Default for tools without a rule — deny is the safe default. */
25
+ readonly default?: "allow" | "deny" | "defer";
26
+ }
27
+ export interface AgentDefinition {
28
+ readonly model: string;
29
+ readonly systemPrompt?: string;
30
+ /** `Tool<any>` like the registry: typed tools register without casts. */
31
+ readonly tools: readonly Tool<any>[];
32
+ readonly store: SessionStore;
33
+ readonly permissionPolicy?: PermissionPolicy;
34
+ /** Raw loop hooks (observers, custom permission logic). */
35
+ readonly hooks?: HookHost;
36
+ /** Direct adapter injection (tests, faux, custom providers). */
37
+ readonly adapter?: Adapter;
38
+ /** Lazy provider: "anthropic" | "openai-compat" (imports the peer package). */
39
+ readonly provider?: "anthropic" | "openai-compat";
40
+ readonly apiKey?: string;
41
+ readonly baseUrl?: string;
42
+ readonly maxTurns?: number;
43
+ readonly maxTokens?: number;
44
+ readonly temperature?: number;
45
+ readonly compaction?: {
46
+ readonly thresholdTokens: number;
47
+ };
48
+ readonly maxRetries?: number;
49
+ }
50
+ export declare class AgentRuntime {
51
+ #private;
52
+ constructor(definition: AgentDefinition);
53
+ sessionIds(): string[];
54
+ /** Session metadata for listings (`kiso sessions`). */
55
+ sessions(): import("./store.js").SessionMeta[];
56
+ /** Release every held fd and writer lock (E 组: the CLI closes on exit). */
57
+ close(): void;
58
+ /** Load an existing session from disk, or create a fresh one. */
59
+ session(options: {
60
+ id: string;
61
+ }): Promise<AgentSession>;
62
+ }
63
+ /** The one-liner the README promises. */
64
+ export declare function createAgent(definition: AgentDefinition): AgentRuntime;
package/dist/agent.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * AgentRuntime + createAgent — the high-level entry point (Phase C).
3
+ *
4
+ * const agent = createAgent({ model, systemPrompt, tools, store,
5
+ * permissionPolicy, provider });
6
+ * const session = await agent.session({ id: "demo" });
7
+ * for await (const event of session.run("Inspect this repository")) { }
8
+ *
9
+ * The runtime is provider-agnostic: an adapter may be injected directly, or
10
+ * a provider name triggers a lazy import of the matching @kiso provider
11
+ * package (optional peers — an unused provider costs nothing). The kernel
12
+ * itself stays dependency-free; the SDKs live in the provider packages.
13
+ */
14
+ import { EventLog, ToolRegistry } from "@vincemakes/kiso-core";
15
+ import { AgentSession } from "./session.js";
16
+ export class AgentRuntime {
17
+ #definition;
18
+ #registry;
19
+ #adapterPromise;
20
+ constructor(definition) {
21
+ this.#definition = definition;
22
+ this.#registry = new ToolRegistry();
23
+ for (const tool of definition.tools)
24
+ this.#registry.register(tool);
25
+ this.#adapterPromise = resolveAdapter(definition);
26
+ }
27
+ sessionIds() {
28
+ return this.#definition.store.list().map((m) => m.id);
29
+ }
30
+ /** Session metadata for listings (`kiso sessions`). */
31
+ sessions() {
32
+ return this.#definition.store.list();
33
+ }
34
+ /** Release every held fd and writer lock (E 组: the CLI closes on exit). */
35
+ close() {
36
+ this.#definition.store.closeAll();
37
+ }
38
+ /** Load an existing session from disk, or create a fresh one. */
39
+ async session(options) {
40
+ const store = this.#definition.store;
41
+ const records = store.load(options.id);
42
+ const log = new EventLog(records.map((r) => r.event));
43
+ const adapter = await this.#adapterPromise;
44
+ const config = {
45
+ model: this.#definition.model,
46
+ ...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
47
+ registry: this.#registry,
48
+ ...(this.#definition.permissionPolicy !== undefined || this.#definition.hooks !== undefined
49
+ ? {
50
+ hooks: {
51
+ ...this.#definition.hooks,
52
+ ...(this.#definition.permissionPolicy !== undefined ? policyHooks(this.#definition.permissionPolicy) : {}),
53
+ },
54
+ }
55
+ : {}),
56
+ ...(this.#definition.maxTurns !== undefined ? { maxTurns: this.#definition.maxTurns } : {}),
57
+ ...(this.#definition.maxTokens !== undefined ? { maxTokens: this.#definition.maxTokens } : {}),
58
+ ...(this.#definition.temperature !== undefined ? { temperature: this.#definition.temperature } : {}),
59
+ ...(this.#definition.compaction !== undefined ? { compaction: this.#definition.compaction } : {}),
60
+ ...(this.#definition.maxRetries !== undefined ? { maxRetries: this.#definition.maxRetries } : {}),
61
+ };
62
+ return new AgentSession(options.id, log, store, adapter, config);
63
+ }
64
+ }
65
+ /** The one-liner the README promises. */
66
+ export function createAgent(definition) {
67
+ return new AgentRuntime(definition);
68
+ }
69
+ /** Wire a PermissionPolicy into the loop's onPreTool hook. */
70
+ function policyHooks(policy) {
71
+ return {
72
+ onPreTool: async (call) => {
73
+ for (const rule of policy.rules) {
74
+ if (rule.tool === call.name) {
75
+ return rule.action === "allow"
76
+ ? { action: "allow" }
77
+ : rule.action === "deny"
78
+ ? { action: "deny", reason: `denied by policy rule for ${call.name}` }
79
+ : { action: "defer" };
80
+ }
81
+ }
82
+ switch (policy.default ?? "deny") {
83
+ case "allow":
84
+ return { action: "allow" };
85
+ case "defer":
86
+ return { action: "defer" };
87
+ default:
88
+ return { action: "deny", reason: `no policy rule for ${call.name} (default deny)` };
89
+ }
90
+ },
91
+ };
92
+ }
93
+ async function resolveAdapter(definition) {
94
+ if (definition.adapter)
95
+ return definition.adapter;
96
+ switch (definition.provider) {
97
+ // 七: the runtime imports ONLY the provider package — its high-level
98
+ // factory owns the SDK and builds the adapter from config. The SDKs
99
+ // are private dependencies of the provider packages, so a nested
100
+ // consumer install resolves them next to the provider, never through
101
+ // a hoisted root that may not exist.
102
+ case "anthropic": {
103
+ const { createAnthropicProvider } = await import("@vincemakes/kiso-provider-anthropic");
104
+ return createAnthropicProvider({
105
+ ...(definition.apiKey !== undefined ? { apiKey: definition.apiKey } : {}),
106
+ ...(definition.baseUrl !== undefined ? { baseUrl: definition.baseUrl } : {}),
107
+ });
108
+ }
109
+ case "openai-compat": {
110
+ const { createOpenAICompatProvider } = await import("@vincemakes/kiso-provider-openai");
111
+ return createOpenAICompatProvider({
112
+ ...(definition.apiKey !== undefined ? { apiKey: definition.apiKey } : {}),
113
+ ...(definition.baseUrl !== undefined ? { baseUrl: definition.baseUrl } : {}),
114
+ });
115
+ }
116
+ default:
117
+ throw new Error("createAgent: pass an `adapter` or a `provider` (\"anthropic\" | \"openai-compat\")");
118
+ }
119
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./agent.js";
2
+ export * from "./session.js";
3
+ export * from "./store.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./agent.js";
2
+ export * from "./session.js";
3
+ export * from "./store.js";
@@ -0,0 +1,155 @@
1
+ /**
2
+ * AgentSession + Run — the durable multi-turn conversation (Phase C/D).
3
+ *
4
+ * A session owns ONE EventLog, seeded from disk on load and continued in
5
+ * memory. Each `run(input)`:
6
+ *
7
+ * 1. appends the user input to the log AND the store (durable first);
8
+ * 2. drives the kernel loop against the session's log — every adapter
9
+ * call is a pure projection of that log (ADR-0002), so multi-turn
10
+ * context is free;
11
+ * 3. writes every event to the store BEFORE yielding it (write-ahead);
12
+ * 4. yields the stream; the run's `runId` and `abort()` ride on the Run
13
+ * handle, not on the event union.
14
+ *
15
+ * Phase D adds the human-in-the-loop surface:
16
+ * - `pendingApprovals()` — pauses that still await a decision
17
+ * (permission_requested without permission_decided);
18
+ * - `approve(decisionId, allow)` — resumes a paused run in-process, or
19
+ * persists the decision directly when the run is gone;
20
+ * - `uncertainExecutions()` / `resolveUncertain(...)` — the ledger of
21
+ * interrupted side effects and the human's rerun/abandon verdict.
22
+ *
23
+ * Restart recovery is the same code path as a second run: rebuild the log
24
+ * from the JSONL, continue numbering where the file ended.
25
+ */
26
+ import { EventLog, type AbortSignalLike, type Adapter, type Event, type Message, type PermissionDecision, type Tool } from "@vincemakes/kiso-core";
27
+ import { type SessionStore } from "./store.js";
28
+ /** A session whose disk write was rejected (stale handle) is PERMANENTLY
29
+ * poisoned: its in-memory log no longer matches the disk, so no further
30
+ * run may proceed — reload the session (一). */
31
+ export declare class PoisonedSessionError extends Error {
32
+ constructor(reason: string);
33
+ }
34
+ export declare class ResumeBlockedError extends Error {
35
+ readonly uncertain: readonly {
36
+ executionId: string;
37
+ callId: string;
38
+ name: string;
39
+ }[];
40
+ constructor(uncertain: readonly {
41
+ executionId: string;
42
+ callId: string;
43
+ name: string;
44
+ }[]);
45
+ }
46
+ export interface ApprovalRequest {
47
+ readonly decisionId: string;
48
+ readonly callId: string;
49
+ readonly name: string;
50
+ readonly input: Readonly<Record<string, unknown>>;
51
+ }
52
+ export declare class AgentSession {
53
+ #private;
54
+ readonly id: string;
55
+ readonly log: EventLog;
56
+ /** Permanently invalidate the session after a rejected disk write (一). */
57
+ poison(reason: string): void;
58
+ ensureHealthy(): void;
59
+ constructor(id: string, log: EventLog, store: SessionStore, adapter: Adapter, config: SessionConfig);
60
+ /** Write-ahead through the store; a rejected write POISONS the session
61
+ * (一/第四轮): the in-memory log no longer matches the disk — whatever
62
+ * the cause (stale handle, corruption, a live external writer, an I/O
63
+ * fault) — so no further run, resume, or log mutation may proceed.
64
+ * The health check runs BEFORE every write, on every path. */
65
+ persist(runId: string, event: Event): Promise<void>;
66
+ beginRun(run: Run): void;
67
+ endRun(run: Run): void;
68
+ /** The conversation so far, as the model sees it. */
69
+ projected(): readonly Message[];
70
+ /** Run one user turn. Iterate to consume; `run.abort()` cancels. */
71
+ run(input: string, options?: {
72
+ signal?: AbortSignalLike;
73
+ }): Run;
74
+ /**
75
+ * Continue the interrupted run (Area 2): apply durable decisions,
76
+ * fill missing receipts, resume the pause, and drive the original
77
+ * trajectory to its terminal — WITHOUT inventing a new user turn.
78
+ * Yields nothing when the session already completed.
79
+ */
80
+ resume(): Run;
81
+ /**
82
+ * Pauses that still await a human decision (durable, survives restart).
83
+ * B 组: a request whose RUN has terminated is DEAD — it is neither
84
+ * re-presented here nor recoverable; expired requests are excluded too.
85
+ */
86
+ pendingApprovals(): ApprovalRequest[];
87
+ /**
88
+ * Answer a pending approval (Area 2). With a live run, the decision is
89
+ * RESOLVED into the run's frame — the loop (or the resume recovery)
90
+ * writes `permission_decided` itself, so there is exactly one writer per
91
+ * event and seq never duplicates. With no live run, the decision is
92
+ * persisted directly (durable, attributed to the original run) and the
93
+ * next resume applies it without re-asking. The crash window between a
94
+ * resolve and the run's write is benign: nothing has executed yet, so a
95
+ * lost decision only re-presents the request.
96
+ */
97
+ approve(decisionId: string, allow: boolean): Promise<void>;
98
+ /** Executions that started but never reported a result (crash window). */
99
+ uncertainExecutions(): import("@vincemakes/kiso-core").ExecutionRecord[];
100
+ /**
101
+ * The human's verdict on an interrupted execution, keyed by EXECUTION ID
102
+ * (B 组): "rerun" (the human says the side effect did NOT happen — the
103
+ * attempt is completed with a recorded failure so the model may re-issue
104
+ * it as a new logical call) or "abandoned" (treated as failed forever).
105
+ * Only uncertain → rerun/abandoned is legal; a resolved or successful
106
+ * execution is left untouched (idempotent, irreversible). Both fill a
107
+ * model-facing result — a dangling tool_use with NO result would be
108
+ * rejected by real providers (review finding 1).
109
+ */
110
+ resolveUncertain(executionId: string, resolution: "rerun" | "abandoned"): Promise<void>;
111
+ /** The runId that owns an execution — from its durable started record. */
112
+ private runIdFor;
113
+ registerUncertaintyResolver(executionId: string, resolve: (resolution: "rerun" | "abandoned") => void): void;
114
+ dropUncertaintyResolver(executionId: string): void;
115
+ registerResolver(decisionId: string, resolve: (decision: PermissionDecision) => void): void;
116
+ /** 第四轮(对抗): a verdict the human already gave for a live decision. */
117
+ approvalVerdict(decisionId: string): boolean | undefined;
118
+ /** 第四轮(对抗): a verdict the human already gave for a live execution. */
119
+ uncertaintyVerdict(executionId: string): "rerun" | "abandoned" | undefined;
120
+ /**
121
+ * 第五轮(P1-5): flush every verdict submitted to a live resolver that is
122
+ * not yet durable. Called from the Run iterator's FINALLY — whether the
123
+ * run completed, aborted, or was abandoned by the consumer. An event the
124
+ * loop already appended is left alone (its persist precedes its yield);
125
+ * a missing event is appended here and persisted, attributed to the run.
126
+ */
127
+ flushPendingVerdicts(runId: string, log: EventLog): Promise<void>;
128
+ dropResolver(decisionId: string): void;
129
+ }
130
+ export interface SessionConfig {
131
+ readonly model: string;
132
+ readonly systemPrompt?: string;
133
+ readonly tools?: readonly Tool<any>[];
134
+ readonly registry: import("@vincemakes/kiso-core").ToolRegistry;
135
+ readonly hooks?: import("@vincemakes/kiso-core").HookHost;
136
+ readonly maxTurns?: number;
137
+ readonly maxTokens?: number;
138
+ readonly temperature?: number;
139
+ readonly compaction?: {
140
+ readonly thresholdTokens: number;
141
+ };
142
+ readonly maxRetries?: number;
143
+ }
144
+ /**
145
+ * A single turn. Async-iterable, so `for await (const ev of session.run(x))`
146
+ * is the natural shape; the handle also carries the runId and the abort.
147
+ */
148
+ export declare class Run implements AsyncIterable<Event> {
149
+ #private;
150
+ runId: string;
151
+ constructor(store: SessionStore, adapter: Adapter, config: SessionConfig, session: AgentSession, input: string | undefined, externalSignal: AbortSignalLike | undefined, resume: boolean);
152
+ /** Cancel the run: propagates to the adapter (SDK) and future executions. */
153
+ abort(): void;
154
+ [Symbol.asyncIterator](): AsyncIterator<Event>;
155
+ }