@irogane/kaji 0.2.0-beta.11 → 0.3.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 +112 -551
- package/dist/index.d.mts +253 -0
- package/dist/index.mjs +440 -0
- package/package.json +28 -166
- package/contracts/README.md +0 -14
- package/contracts/beta-core-v1.json +0 -52
- package/contracts/cli/init-cases-v1.json +0 -27
- package/contracts/errors/error-codes.json +0 -48
- package/contracts/errors/integration-recovery-v1.json +0 -127
- package/contracts/errors/provider-normalization.json +0 -111
- package/contracts/events/conformance-invalid.json +0 -20
- package/contracts/events/conformance.json +0 -511
- package/contracts/events/new-kaji-event-v1.schema.json +0 -1021
- package/contracts/events/stored-kaji-event-v1.schema.json +0 -1025
- package/contracts/feature-tiers-v1.json +0 -488
- package/contracts/integrations/abi-index-v1.json +0 -8
- package/contracts/integrations/conformance-invalid.json +0 -443
- package/contracts/integrations/conformance-valid.json +0 -121
- package/contracts/integrations/copy-provenance-v1.schema.json +0 -61
- package/contracts/integrations/echo-tool-abi-v1.json +0 -37
- package/contracts/integrations/github-api-conformance-v1.json +0 -644
- package/contracts/integrations/github-tool-abi-typescript-v1.json +0 -369
- package/contracts/integrations/github-tool-abi-v1.json +0 -146
- package/contracts/integrations/gmail-api-conformance-v1.json +0 -750
- package/contracts/integrations/gmail-tool-abi-v1.json +0 -62
- package/contracts/integrations/index.schema.json +0 -37
- package/contracts/integrations/manifest.schema.json +0 -119
- package/contracts/parity/expected-normalized.json +0 -4906
- package/contracts/parity/scenarios.json +0 -100
- package/contracts/parity/scenarios.schema.json +0 -214
- package/contracts/providers/cost-conformance.json +0 -112
- package/contracts/release/github-proof-v1.schema.json +0 -138
- package/contracts/release/gmail-proof-v1.schema.json +0 -138
- package/contracts/release/kaji-ts-consumer-handoff-v1.schema.json +0 -1289
- package/contracts/release/publisher-identity-receipt-v1.schema.json +0 -319
- package/contracts/release/typescript-onboarding-evidence-v1.schema.json +0 -740
- package/contracts/tools/conformance-invalid.json +0 -271
- package/contracts/tools/conformance-valid.json +0 -78
- package/contracts/tools/tool-schema-v1.schema.json +0 -18
- package/dist/anthropic.cjs +0 -1231
- package/dist/anthropic.cjs.map +0 -1
- package/dist/anthropic.d.cts +0 -26
- package/dist/anthropic.d.ts +0 -26
- package/dist/anthropic.js +0 -270
- package/dist/anthropic.js.map +0 -1
- package/dist/auth.cjs +0 -1507
- package/dist/auth.cjs.map +0 -1
- package/dist/auth.d.cts +0 -129
- package/dist/auth.d.ts +0 -129
- package/dist/auth.js +0 -1039
- package/dist/auth.js.map +0 -1
- package/dist/base-B9FRMcP8.d.cts +0 -140
- package/dist/base-nHQd1VtS.d.ts +0 -140
- package/dist/chunk-AAM33KAO.js +0 -4367
- package/dist/chunk-AAM33KAO.js.map +0 -1
- package/dist/chunk-KAJ6BM64.js +0 -153
- package/dist/chunk-KAJ6BM64.js.map +0 -1
- package/dist/chunk-KCAXIOZS.js +0 -308
- package/dist/chunk-KCAXIOZS.js.map +0 -1
- package/dist/chunk-LSJ4AVO2.js +0 -243
- package/dist/chunk-LSJ4AVO2.js.map +0 -1
- package/dist/chunk-TM7ZGOJX.js +0 -716
- package/dist/chunk-TM7ZGOJX.js.map +0 -1
- package/dist/cli/bin.d.ts +0 -2
- package/dist/cli/bin.js +0 -13
- package/dist/cli/bin.js.map +0 -1
- package/dist/cli/chunk-2RCWPRVY.js +0 -6277
- package/dist/cli/chunk-2RCWPRVY.js.map +0 -1
- package/dist/cli/chunk-SEBX54TR.js +0 -681
- package/dist/cli/chunk-SEBX54TR.js.map +0 -1
- package/dist/cli/index.d.ts +0 -232
- package/dist/cli/index.js +0 -11
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/init-worker.d.ts +0 -2
- package/dist/cli/init-worker.js +0 -18
- package/dist/cli/init-worker.js.map +0 -1
- package/dist/cli/integration-copy-worker.js +0 -48
- package/dist/cli/integration-copy-worker.js.map +0 -1
- package/dist/cli/package-entry-cjs.cjs +0 -21
- package/dist/cli/package-entry-cjs.cjs.map +0 -1
- package/dist/cli/package-entry-cjs.d.cts +0 -2
- package/dist/cli/package-entry.d.ts +0 -2
- package/dist/cli/package-entry.js +0 -13
- package/dist/cli/package-entry.js.map +0 -1
- package/dist/context-BaFHrQHv.d.cts +0 -21
- package/dist/context-BaFHrQHv.d.ts +0 -21
- package/dist/context-C-YPY-GS.d.cts +0 -1538
- package/dist/context-C-YPY-GS.d.ts +0 -1538
- package/dist/index.cjs +0 -12346
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -1560
- package/dist/index.d.ts +0 -1560
- package/dist/index.js +0 -7852
- package/dist/index.js.map +0 -1
- package/dist/integrations/github.cjs +0 -2092
- package/dist/integrations/github.cjs.map +0 -1
- package/dist/integrations/github.d.cts +0 -21
- package/dist/integrations/github.d.ts +0 -21
- package/dist/integrations/github.js +0 -2088
- package/dist/integrations/github.js.map +0 -1
- package/dist/integrations.cjs +0 -3370
- package/dist/integrations.cjs.map +0 -1
- package/dist/integrations.d.cts +0 -202
- package/dist/integrations.d.ts +0 -202
- package/dist/integrations.js +0 -2650
- package/dist/integrations.js.map +0 -1
- package/dist/observability-Cj--OkME.d.cts +0 -96
- package/dist/observability-Cj--OkME.d.ts +0 -96
- package/dist/openai.cjs +0 -1235
- package/dist/openai.cjs.map +0 -1
- package/dist/openai.d.cts +0 -32
- package/dist/openai.d.ts +0 -32
- package/dist/openai.js +0 -272
- package/dist/openai.js.map +0 -1
- package/dist/testing.cjs +0 -554
- package/dist/testing.cjs.map +0 -1
- package/dist/testing.d.cts +0 -43
- package/dist/testing.d.ts +0 -43
- package/dist/testing.js +0 -118
- package/dist/testing.js.map +0 -1
- package/registry/echo/index.ts +0 -53
- package/registry/echo/manifest.json +0 -53
- package/registry/github/LICENSE +0 -105
- package/registry/github/client.ts +0 -1727
- package/registry/github/index.ts +0 -263
- package/registry/github/manifest.json +0 -227
- package/registry/github/owner-fixtures.json +0 -10
- package/registry/github/tests/github.test.ts +0 -32
- package/registry/gmail/LICENSE +0 -105
- package/registry/gmail/client.ts +0 -548
- package/registry/gmail/index.ts +0 -165
- package/registry/gmail/manifest.json +0 -102
- package/registry/gmail/owner-fixtures.json +0 -10
- package/registry/gmail/tests/gmail.test.ts +0 -32
- package/registry/index.json +0 -21
- package/registry/index.schema.json +0 -37
- package/registry/schema.json +0 -119
package/README.md
CHANGED
|
@@ -1,598 +1,159 @@
|
|
|
1
|
-
# Kaji
|
|
1
|
+
# Kaji
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
server, or environment configured). It mirrors the runtime core of the Python
|
|
6
|
-
`kaji` SDK.
|
|
3
|
+
Kaji is an embedded execution layer for safely turning agent-requested
|
|
4
|
+
application actions into real state changes.
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
local/test default.
|
|
17
|
-
|
|
18
|
-
See [**Kaji MVP**](https://github.com/enkyuan/alloy/blob/main/docs/MVP.md) for
|
|
19
|
-
the full five-step developer path and scope definition.
|
|
20
|
-
|
|
21
|
-
## Install
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm install @irogane/kaji@0.2.0-beta.11 zod openai # OpenAI
|
|
25
|
-
# or
|
|
26
|
-
npm install @irogane/kaji@0.2.0-beta.11 zod @anthropic-ai/sdk # Anthropic (experimental/WIP)
|
|
27
|
-
# or: bun add @irogane/kaji@0.2.0-beta.11 zod openai
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
`zod` is a required peer dependency (Zod 4). `openai` and `@anthropic-ai/sdk`
|
|
31
|
-
are optional peers -- install only the one you use. Anthropic is not in the
|
|
32
|
-
beta support tier. The package metadata declares Node `22.x || 24.x`, but the
|
|
33
|
-
protected beta onboarding evidence is deliberately narrower: it proves npm and
|
|
34
|
-
Bun on GitHub-hosted Linux/x64, with Node 22 on `ubuntu-22.04` and Node 24 on
|
|
35
|
-
`ubuntu-24.04`. It makes no broader runtime or platform claim, including for
|
|
36
|
-
macOS/arm64, Windows, or fully offline dependency installation. The protected
|
|
37
|
-
compiler cells use TypeScript 5.7.3 and 6.0.3.
|
|
38
|
-
|
|
39
|
-
## Quick start
|
|
40
|
-
|
|
41
|
-
First prove a text-only turn without credentials. No principal is required
|
|
42
|
-
because this runtime has no enabled tools. Save this example as
|
|
43
|
-
`quickstart.mts` so top-level await runs as ESM, including from npm's default
|
|
44
|
-
CommonJS project:
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
import { AgentBuilder } from "@irogane/kaji";
|
|
48
|
-
import { MockProvider } from "@irogane/kaji/testing";
|
|
49
|
-
|
|
50
|
-
const runtime = new AgentBuilder().provider(new MockProvider({ reply: "hello" })).build();
|
|
51
|
-
const result = await runtime.turn("Say hello.");
|
|
52
|
-
console.log(result.text, result.accounting);
|
|
6
|
+
```text
|
|
7
|
+
agent / framework / direct caller
|
|
8
|
+
↓
|
|
9
|
+
Kaji
|
|
10
|
+
↓
|
|
11
|
+
application capability
|
|
12
|
+
↓
|
|
13
|
+
application / external system
|
|
53
14
|
```
|
|
54
15
|
|
|
55
|
-
|
|
16
|
+
Your application already has functions like `refundPayment()` or
|
|
17
|
+
`cancelBooking()`. When something outside your direct control — an
|
|
18
|
+
agent, an automation, an MCP tool call — can trigger one of those
|
|
19
|
+
functions, it needs caller identity, input validation, authorization,
|
|
20
|
+
approval, and protection against duplicate or ambiguous side effects.
|
|
21
|
+
Kaji standardizes that boundary once, inside your application, instead
|
|
22
|
+
of once per caller.
|
|
56
23
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
> provider-derived text and deltas, tool arguments, tool results, and arbitrary
|
|
60
|
-
> metadata. They are not redaction-safe; never log, attach, or export them
|
|
61
|
-
> wholesale.
|
|
24
|
+
Kaji is a TypeScript library with zero runtime dependencies. It runs in
|
|
25
|
+
your process — no server, database, or control plane.
|
|
62
26
|
|
|
63
|
-
|
|
64
|
-
not complete business or audit records. Sink failures are swallowed so
|
|
65
|
-
observability cannot change turn behavior. Traces still contain access-controlled
|
|
66
|
-
correlation identifiers even though they omit full event payloads.
|
|
67
|
-
|
|
68
|
-
Failed turns throw: failed turns have no `TurnResult` and therefore no
|
|
69
|
-
`TurnResult.events` or successful-turn `TurnAccounting` aggregate. Applications
|
|
70
|
-
that need failure evidence must choose a preselected session ID, retain the
|
|
71
|
-
caught error separately for live control flow, page history with an
|
|
72
|
-
exclusive `afterSequence` cursor until an empty page, and reduce to an allowlist
|
|
73
|
-
before export; generic provider failures have no durable recovery code today. Use the
|
|
74
|
-
caught typed provider error and `normalizeProviderError()` where applicable.
|
|
75
|
-
Always page until an empty page; a short page is not proof of exhaustion.
|
|
27
|
+
## Install
|
|
76
28
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const events: StoredKajiEvent[] = [];
|
|
82
|
-
let afterSequence = 0;
|
|
83
|
-
for (;;) {
|
|
84
|
-
const page = await runtime.history(sessionId, { afterSequence, limit });
|
|
85
|
-
if (page.length === 0) return events;
|
|
86
|
-
const nextSequence = page.at(-1)!.sequence;
|
|
87
|
-
if (nextSequence <= afterSequence) throw new Error("history cursor did not advance");
|
|
88
|
-
events.push(...page);
|
|
89
|
-
afterSequence = nextSequence;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const SAFE_FIELDS = [
|
|
94
|
-
"tool_name",
|
|
95
|
-
"tool_call_id",
|
|
96
|
-
"error_code",
|
|
97
|
-
"phase",
|
|
98
|
-
"retryable",
|
|
99
|
-
"outcome",
|
|
100
|
-
"reason_code",
|
|
101
|
-
"recovery_code",
|
|
102
|
-
"doc_url",
|
|
103
|
-
] as const;
|
|
104
|
-
|
|
105
|
-
function safeJournalEvidence(event: StoredKajiEvent): Record<string, unknown> {
|
|
106
|
-
const safe: Record<string, unknown> = { sequence: event.sequence, type: event.type };
|
|
107
|
-
if (event.turn_id !== undefined) safe.turn_id = event.turn_id;
|
|
108
|
-
for (const field of SAFE_FIELDS) {
|
|
109
|
-
const value = Reflect.get(event, field);
|
|
110
|
-
if (value !== undefined) safe[field] = value;
|
|
111
|
-
}
|
|
112
|
-
return safe; // never content, delta, tool_args, result, metadata, or raw session_id
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const sessionId = crypto.randomUUID();
|
|
116
|
-
let failure: { error: unknown } | undefined;
|
|
117
|
-
try {
|
|
118
|
-
await runtime.turn("Investigate the failure.", { sessionId });
|
|
119
|
-
} catch (error) {
|
|
120
|
-
failure = { error }; // retain separately; never add this value to safe evidence
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (failure !== undefined) {
|
|
124
|
-
stopIngress(sessionId);
|
|
125
|
-
await runtime.drainTools(10_000);
|
|
126
|
-
await runtime.drainProviders(10_000);
|
|
127
|
-
try {
|
|
128
|
-
const privilegedHistory = await pageHistory(runtime, sessionId);
|
|
129
|
-
const exportableEvidence = privilegedHistory.map(safeJournalEvidence);
|
|
130
|
-
sendToYourIncidentStore(exportableEvidence);
|
|
131
|
-
} catch (evidenceError) {
|
|
132
|
-
handleEvidenceExportError(evidenceError); // report separately; original failure stays authoritative
|
|
133
|
-
} finally {
|
|
134
|
-
await runtime.purgeSession(sessionId);
|
|
135
|
-
}
|
|
136
|
-
handleOriginalError(failure.error); // cleanup finished; preserve original control flow
|
|
137
|
-
}
|
|
138
|
-
```
|
|
29
|
+
Kaji v0 is not yet published to npm. Publishing an install command now
|
|
30
|
+
would resolve to the previously published agent SDK under this name
|
|
31
|
+
(0.2.0-beta.11), so the command is enabled only when the v0 artifact
|
|
32
|
+
is published.
|
|
139
33
|
|
|
140
|
-
|
|
141
|
-
race the drain-to-purge interval. On a live runtime, drain tools and providers,
|
|
142
|
-
page and reduce any required evidence, then call `purgeSession(sessionId)` while
|
|
143
|
-
leaving other sessions running. For whole-runtime shutdown, `close()` may run
|
|
144
|
-
first to reject future turn APIs; history and purge remain callable afterward.
|
|
145
|
-
`close()` does not delete retained history and does not cancel already-active
|
|
146
|
-
work.
|
|
147
|
-
|
|
148
|
-
This lifecycle is identical to Python's supported in-memory path. The bounded
|
|
149
|
-
store never evicts a retained session implicitly: a full store raises
|
|
150
|
-
`EventStoreCapacityError` until the host explicitly purges one. Runtime purge
|
|
151
|
-
closes its old subscribers, removes the event and ID indexes, and clears every
|
|
152
|
-
runtime owner sharing the store; a standalone raw listener must be closed by
|
|
153
|
-
its caller. Reset cursors to `0` before reuse; the next generation begins at
|
|
154
|
-
sequence `1`.
|
|
155
|
-
|
|
156
|
-
`PurgeableEventStore.purgeSession(sessionId)` is the public one-argument
|
|
157
|
-
store-only capability, detected by `supportsSessionPurge()`. Runtime purge also
|
|
158
|
-
requires Kaji's internal opaque coordinated capability, so a custom store that
|
|
159
|
-
implements only the public method fails closed at the runtime boundary. Direct
|
|
160
|
-
store operations and new owners remain fenced throughout purge. Split delivery
|
|
161
|
-
is unsupported because its outbox cannot cross a reused generation.
|
|
162
|
-
|
|
163
|
-
TypeScript's embedded defaults are bounded, in-memory, and process-local. This
|
|
164
|
-
beta ships no persistent event store or distributed coordinator and does not
|
|
165
|
-
release-certify host implementations; durability, deletion, and cross-process
|
|
166
|
-
correctness are host responsibilities. `purgeSession()` removes SDK-owned
|
|
167
|
-
retained indexes and caches but cannot promise VM string zeroization or erase
|
|
168
|
-
copies already sent to logs, sinks, providers, custom stores, crash dumps, or
|
|
169
|
-
caller-owned objects.
|
|
170
|
-
|
|
171
|
-
A custom `ToolIdempotencyLedger` must implement optional `releaseSettled()` to
|
|
172
|
-
participate in purge. The event store and SDK caches are already cleared before
|
|
173
|
-
host ledger cleanup is awaited. If that cleanup rejects, deletion cannot be
|
|
174
|
-
rolled back; repair the host ledger and retry the named purge. If it never
|
|
175
|
-
settles, the strong `cleanup_pending` tombstone keeps turns, direct store
|
|
176
|
-
operations, subscriptions, and new owners fenced. A later runtime purge retries
|
|
177
|
-
cleanup without repeating physical deletion. Kaji cannot force hostile
|
|
178
|
-
in-process host code to settle. `TurnAccounting` remains TypeScript-only and is
|
|
179
|
-
separate from the cross-SDK purge contract.
|
|
180
|
-
|
|
181
|
-
Then set an API key and add a risk-classified tool with explicit caller
|
|
182
|
-
identity, deadline, and cancellation:
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
export OPENAI_API_KEY=sk-...
|
|
186
|
-
# Experimental/WIP alternative: export ANTHROPIC_API_KEY=sk-ant-...
|
|
187
|
-
```
|
|
34
|
+
## Minimal example
|
|
188
35
|
|
|
189
36
|
```ts
|
|
190
|
-
import {
|
|
37
|
+
import { capability, createKaji, knownFailure, memoryStore } from "@irogane/kaji";
|
|
191
38
|
import { z } from "zod";
|
|
192
39
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
readonly getWeather = tool(
|
|
197
|
-
{
|
|
198
|
-
description: "Return weather for a city.",
|
|
199
|
-
parameters: z.object({ city: z.string() }),
|
|
200
|
-
risk: "read",
|
|
201
|
-
},
|
|
202
|
-
async (args, _context) => ({ city: args.city, tempF: 68 }),
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const runtime = new AgentBuilder()
|
|
207
|
-
.provider(new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! }))
|
|
208
|
-
.integration(new WeatherIntegration())
|
|
209
|
-
.systemPrompt("You are a weather assistant.")
|
|
210
|
-
.build();
|
|
211
|
-
|
|
212
|
-
const result = await runtime.turn("Weather in Seattle?", {
|
|
213
|
-
context: {
|
|
214
|
-
principalId: "weather-app",
|
|
215
|
-
deadlineAtMs: deadlineAfter(30_000),
|
|
216
|
-
},
|
|
40
|
+
const RefundInput = z.object({
|
|
41
|
+
paymentId: z.string().min(1),
|
|
42
|
+
amount: z.number().positive(),
|
|
217
43
|
});
|
|
218
|
-
console.log(result.text, result.accounting);
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Swap `OpenAIProvider` for `AnthropicProvider` (and `OPENAI_API_KEY` for
|
|
222
|
-
`ANTHROPIC_API_KEY`) to use the experimental/WIP Anthropic adapter.
|
|
223
44
|
|
|
224
|
-
|
|
225
|
-
|
|
45
|
+
const refund = capability({
|
|
46
|
+
name: "payments.refund",
|
|
47
|
+
input: RefundInput,
|
|
226
48
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
integration without copying its source into your application. For an
|
|
231
|
-
investigation agent, opt into read-only exposure so the two mutation tools are
|
|
232
|
-
not registered or sent to the model.
|
|
49
|
+
authorize: async ({ principalId, input }) => {
|
|
50
|
+
return canRefund(principalId, input.paymentId);
|
|
51
|
+
},
|
|
233
52
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return token;
|
|
53
|
+
approval: ({ input }) => input.amount >= 500,
|
|
54
|
+
|
|
55
|
+
execute: async (input, context) => {
|
|
56
|
+
try {
|
|
57
|
+
return await payments.refund({
|
|
58
|
+
...input,
|
|
59
|
+
idempotencyKey: context.idempotencyKey,
|
|
60
|
+
signal: context.signal,
|
|
61
|
+
});
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
if (isDefinitiveProviderRejection(cause)) throw knownFailure(cause);
|
|
64
|
+
throw cause; // anything else stays unknown: the side effect may have committed
|
|
65
|
+
}
|
|
248
66
|
},
|
|
249
67
|
});
|
|
250
68
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.defaultContext({ principalId })
|
|
255
|
-
.systemPrompt("Use GitHub evidence from owner/repo and cite immutable refs.")
|
|
256
|
-
.build();
|
|
257
|
-
|
|
258
|
-
const result = await runtime.turn("Inspect the latest failed checks.", {
|
|
259
|
-
context: { deadlineAtMs: deadlineAfter(30_000) },
|
|
69
|
+
const kaji = createKaji({
|
|
70
|
+
store: memoryStore(),
|
|
71
|
+
approve: async () => ({ approved: true }),
|
|
260
72
|
});
|
|
261
|
-
console.log(result.text);
|
|
262
|
-
github.close();
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
`toolExposure` controls which tools reach the model; it does not reduce token
|
|
266
|
-
permissions. Keep the repository allowlist narrow and use a fine-grained token
|
|
267
|
-
with only the read permissions needed by the selected tools. The callback is
|
|
268
|
-
lazy, receives the tool execution context, and returns the raw token; Kaji
|
|
269
|
-
validates it and adds the `Bearer` header. `AgentBuilder` does not own the
|
|
270
|
-
integration, so close it only after active tool work has settled. See the
|
|
271
|
-
[GitHub integration guide](https://github.com/enkyuan/alloy/blob/main/apps/docs/content/integrations/github.mdx)
|
|
272
|
-
for the 15-tool catalog, mutation policy, limits, and unsupported surfaces.
|
|
273
|
-
|
|
274
|
-
### Gmail integration (experimental)
|
|
275
|
-
|
|
276
|
-
Gmail ships as an **experimental** catalog entry. Unlike `github`, it has no
|
|
277
|
-
`@irogane/kaji/integrations/gmail` package subpath. You copy its source into your
|
|
278
|
-
project and own it:
|
|
279
73
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
The copied `index.ts` exports `createGmailIntegration({ tokenFor })`, where
|
|
285
|
-
`tokenFor` is a lazy callback that returns the OAuth access token for the
|
|
286
|
-
`gmail.readonly` and `gmail.send` scopes. Wire it into `AgentBuilder` exactly
|
|
287
|
-
like the GitHub integration:
|
|
288
|
-
|
|
289
|
-
```ts
|
|
290
|
-
import { createGmailIntegration } from "./integrations/gmail";
|
|
291
|
-
|
|
292
|
-
const gmail = createGmailIntegration({
|
|
293
|
-
tokenFor: async (context) => {
|
|
294
|
-
if (context.signal.aborted) throw context.signal.reason;
|
|
295
|
-
const token = process.env.GMAIL_ACCESS_TOKEN;
|
|
296
|
-
if (!token) throw new Error("GMAIL_ACCESS_TOKEN is required");
|
|
297
|
-
return token;
|
|
298
|
-
},
|
|
74
|
+
const result = await kaji.execute(refund, {
|
|
75
|
+
input: { paymentId: "pay_123", amount: 750 },
|
|
76
|
+
principalId: "user_123",
|
|
77
|
+
idempotencyKey: "refund_req_123",
|
|
299
78
|
});
|
|
300
|
-
// ...builder.integration(gmail)... then gmail.close() after tool work settles.
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
It exposes three tools: `list_messages` (with `page_token`/`next_page_token`
|
|
304
|
-
pagination), `get_message` (first `text/plain` body, decoded and bounded), and
|
|
305
|
-
`send_message`, an external-effect write whose ambiguous failures surface the
|
|
306
|
-
`gmail_mutation_unknown` recovery reason rather than risking a silent double
|
|
307
|
-
send. The client is fixed to `https://gmail.googleapis.com`, rejects redirects,
|
|
308
|
-
and bounds every response. See the
|
|
309
|
-
[Gmail integration guide](https://github.com/enkyuan/alloy/blob/main/apps/docs/content/integrations/index.mdx)
|
|
310
|
-
for scopes, the `kaji connect gmail` grant flow, and limits.
|
|
311
|
-
|
|
312
|
-
`deadlineAtMs` is an absolute Unix epoch value; use `deadlineAfter()` when the
|
|
313
|
-
caller has a duration. An earlier caller deadline can tighten, but never extend,
|
|
314
|
-
the configured 120-second whole-turn default covering queue wait, provider open
|
|
315
|
-
and streaming, approval, and tool work. Cooperative provider shutdown may use
|
|
316
|
-
the additional configured cancellation grace.
|
|
317
|
-
|
|
318
|
-
Catch a Kaji `ProviderError`, then call `normalizeProviderError(error)` for the
|
|
319
|
-
redaction-safe `type`, `code`, `service`, `action`, `status`, and `retryable`
|
|
320
|
-
fields. The normalizer accepts Kaji provider errors, not arbitrary vendor
|
|
321
|
-
exceptions.
|
|
322
|
-
|
|
323
|
-
See [`docs/kaji/production-beta.md`](https://github.com/enkyuan/alloy/blob/main/docs/kaji/production-beta.md) for
|
|
324
|
-
the installed-package version of both first-success examples and exact default
|
|
325
|
-
limits. Operating details are in
|
|
326
|
-
[`concurrency-and-ordering.md`](https://github.com/enkyuan/alloy/blob/main/docs/kaji/concurrency-and-ordering.md),
|
|
327
|
-
[`tool-contracts.md`](https://github.com/enkyuan/alloy/blob/main/docs/kaji/tool-contracts.md), and
|
|
328
|
-
[`troubleshooting.md`](https://github.com/enkyuan/alloy/blob/main/docs/kaji/troubleshooting.md).
|
|
329
|
-
Call `runtime.effectiveLimits()` to inspect the immutable
|
|
330
|
-
`EffectiveRuntimeLimits` resolved for one runtime.
|
|
331
|
-
|
|
332
|
-
Runtimes that share the same `EventStore` also share a default per-store turn
|
|
333
|
-
coordinator within the current process, so same-session turns serialize even
|
|
334
|
-
when separate builders create the runtimes. Different stores do not block one
|
|
335
|
-
another. This is not a distributed lock: multi-process deployments must inject
|
|
336
|
-
a `SessionTurnCoordinator` backed by shared infrastructure.
|
|
337
|
-
|
|
338
|
-
## Prove it with a model
|
|
339
|
-
|
|
340
|
-
OpenAI with `gpt-5.4-mini` is the recommended first live check because it is
|
|
341
|
-
cost-effective and exercises the SDK's Chat Completions tool path.
|
|
342
|
-
|
|
343
|
-
```bash
|
|
344
|
-
bun --filter kaji test:quickstart
|
|
345
|
-
OPENAI_API_KEY=... KAJI_LIVE_OPENAI_MODEL=gpt-5.4-mini \
|
|
346
|
-
bun --filter kaji test:integration tests/integration/openai-tools.test.ts
|
|
347
79
|
```
|
|
348
80
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
Keyed OpenAI proof requires protected tool loops in both SDKs on one exact
|
|
352
|
-
commit. A missing `OPENAI_API_KEY` blocks that release evidence. Anthropic,
|
|
353
|
-
Kimi, Gemini, and OpenRouter are experimental/WIP; the latter three are
|
|
354
|
-
OpenAI-compatible factories rather than native provider implementations.
|
|
81
|
+
`canRefund()` and `payments.refund()` are your own application code.
|
|
82
|
+
Kaji never sees your domain logic — it only governs when `execute` runs.
|
|
355
83
|
|
|
356
|
-
|
|
84
|
+
## Why Kaji
|
|
357
85
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
86
|
+
- **Explicit identity.** Every execution requires a caller-supplied
|
|
87
|
+
`principalId`. Kaji never infers one from process state.
|
|
88
|
+
- **Validation before anything else.** Invalid input never reaches
|
|
89
|
+
`authorize`, `approval`, or `execute`.
|
|
90
|
+
- **Duplicate suppression.** A caller-supplied `idempotencyKey` claims
|
|
91
|
+
one intended operation; a repeated request with the same key replays
|
|
92
|
+
the recorded result instead of running `execute` again.
|
|
93
|
+
- **Fail-closed approval.** A capability declares when approval is
|
|
94
|
+
required; the host application supplies how a decision is obtained;
|
|
95
|
+
a missing, rejected, or failed decision prevents execution.
|
|
96
|
+
- **Explicit failure semantics.** `succeeded`, `failed`, and `unknown`
|
|
97
|
+
are distinct outcomes — see below.
|
|
361
98
|
|
|
362
|
-
|
|
363
|
-
checks, TS package smoke, mandatory pinned ast-grep boundary checks, and no-key
|
|
364
|
-
live-gate hygiene. The ast-grep step guards the Python SDK/service boundary, core package dependency direction, removed tool-model imports, TypeScript optional provider imports, and cancellation error shape.
|
|
99
|
+
## Execution outcomes
|
|
365
100
|
|
|
366
|
-
|
|
101
|
+
`kaji.execute()` resolves with an explicit `status` for every governed
|
|
102
|
+
execution; it does not throw for expected outcomes.
|
|
367
103
|
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
```
|
|
104
|
+
```text
|
|
105
|
+
execute() resolves
|
|
106
|
+
→ succeeded
|
|
372
107
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
`OPENAI_API_KEY` and fails when it is absent.
|
|
108
|
+
execute() throws knownFailure(cause)
|
|
109
|
+
→ failed
|
|
376
110
|
|
|
377
|
-
|
|
378
|
-
|
|
111
|
+
execute() throws or rejects with anything else
|
|
112
|
+
→ unknown
|
|
379
113
|
```
|
|
380
114
|
|
|
381
|
-
`
|
|
382
|
-
|
|
383
|
-
|
|
115
|
+
`unknown` means the side effect may have committed, but Kaji cannot
|
|
116
|
+
prove the final result — a thrown error, a timeout, or a cancellation
|
|
117
|
+
after `execute()` has started all settle `unknown`, never `failed`.
|
|
118
|
+
Kaji never infers a known failure from an error's class, message, or
|
|
119
|
+
status code; only application code that can prove no side effect
|
|
120
|
+
committed may throw `knownFailure()`. See the
|
|
121
|
+
[execution outcomes guide](https://kaji.build/docs/concepts/outcomes)
|
|
122
|
+
for the full model.
|
|
384
123
|
|
|
385
|
-
|
|
386
|
-
OPENAI_API_KEY=... \
|
|
387
|
-
KAJI_RELEASE_ARTIFACTS_DIR="$PWD/.artifacts/kaji-release" \
|
|
388
|
-
KAJI_RELEASE_COMMIT=<40-character-commit> KAJI_RUN_KEYED_LIVE=1 \
|
|
389
|
-
uv run --project kaji/packages/py python kaji/scripts/beta_release_check.py
|
|
390
|
-
```
|
|
124
|
+
## memoryStore()
|
|
391
125
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
`kaji-onboarding` protects deterministic TypeScript onboarding,
|
|
395
|
-
`kaji-release` protects keyed OpenAI proof, and `kaji-publish` protects
|
|
396
|
-
publisher identity and the sole npm write. The single-provider command above
|
|
397
|
-
is only a local paid smoke test.
|
|
398
|
-
|
|
399
|
-
## Stability tiers
|
|
400
|
-
|
|
401
|
-
- **Stable core:** `AgentBuilder`, `AgentRuntime`, `ToolRegistry`,
|
|
402
|
-
`ToolPlanner`, session replay, the OpenAI provider, and the in-memory
|
|
403
|
-
event store/committer form the supported embedded-agent surface.
|
|
404
|
-
- **Experimental providers:** Anthropic, Gemini, Kimi, and OpenRouter remain
|
|
405
|
-
opt-in WIP surfaces without a beta compatibility or publication-proof
|
|
406
|
-
commitment.
|
|
407
|
-
- **Experimental Python-only:** Redis realtime/history, voice/TTS,
|
|
408
|
-
`DocumentRAG`, native Gemini/Kimi providers, tool retrieval, and text/voice
|
|
409
|
-
modalities exist in Python but are not production-hardened.
|
|
410
|
-
- **TS not ported:** Redis realtime, voice/TTS, and RAG are not implemented in
|
|
411
|
-
TypeScript. TS Gemini/Kimi remain OpenAI-compatible factories rather than
|
|
412
|
-
native provider implementations.
|
|
413
|
-
|
|
414
|
-
See https://github.com/enkyuan/alloy/blob/main/kaji/RELEASE_MATRIX.md for the
|
|
415
|
-
cross-SDK release matrix and the exact distinction between stable core,
|
|
416
|
-
experimental Python-only surfaces, and TypeScript surfaces that are not ported.
|
|
417
|
-
|
|
418
|
-
The beta promise is the core agent loop. Redis realtime/history, voice/TTS,
|
|
419
|
-
`DocumentRAG`, native Gemini/Kimi, and tool retrieval remain outside the beta
|
|
420
|
-
gate until the promotion criteria in `kaji/RELEASE_MATRIX.md` are met.
|
|
421
|
-
Gemini and Kimi are OpenAI-compatible factories in TypeScript, not native
|
|
422
|
-
provider implementations.
|
|
423
|
-
|
|
424
|
-
## Approval handler
|
|
425
|
-
|
|
426
|
-
Tools whose risk exceeds your policy threshold pause for approval before the
|
|
427
|
-
runtime executes them. All hosts use `TypedApprovalHandler` and return an
|
|
428
|
-
`ApprovalDecision`. `cliApprovalHandler` is a typed dev/REPL implementation
|
|
429
|
-
that prints the tool name, risk, and arguments, then reads `y` / `N` on stdin:
|
|
126
|
+
`createKaji()` needs an `ExecutionStore`. `memoryStore()` is the
|
|
127
|
+
built-in reference implementation:
|
|
430
128
|
|
|
431
129
|
```ts
|
|
432
|
-
import {
|
|
433
|
-
|
|
434
|
-
const agent = new AgentBuilder()
|
|
435
|
-
.provider(openai())
|
|
436
|
-
.approvalHandler(cliApprovalHandler({ label: "agent-a" }))
|
|
437
|
-
.build();
|
|
438
|
-
```
|
|
130
|
+
import { memoryStore } from "@irogane/kaji";
|
|
439
131
|
|
|
440
|
-
|
|
441
|
-
an `ApprovalDecision`, for example
|
|
442
|
-
`{ granted: true, code: "approved" }` or a rejected decision with an explicit
|
|
443
|
-
code and safe reason. See
|
|
444
|
-
[`tool-contracts.md`](https://github.com/enkyuan/alloy/blob/main/docs/kaji/tool-contracts.md) for the lifecycle.
|
|
445
|
-
|
|
446
|
-
`EventApprovalHandler` requires a non-empty turn ID and accepts a decision only
|
|
447
|
-
when `turn_id`, `tool_call_id`, and `tool_name` all match the pending request.
|
|
448
|
-
Unscoped or stale backlog decisions are ignored.
|
|
449
|
-
|
|
450
|
-
## CLI
|
|
451
|
-
|
|
452
|
-
```
|
|
453
|
-
kaji --help # list subcommands
|
|
454
|
-
kaji add <integration> # copy an integration into your project
|
|
455
|
-
kaji add <integration> --allow-experimental # explicitly copy a non-beta template
|
|
456
|
-
kaji init [path] --provider mock --yes # no-key TypeScript scaffold
|
|
457
|
-
kaji list-integrations # enumerate the registry catalog
|
|
458
|
-
kaji replay <session.jsonl> # render a stored JSONL session log
|
|
132
|
+
const kaji = createKaji({ store: memoryStore() });
|
|
459
133
|
```
|
|
460
134
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
135
|
+
`memoryStore()` is process-local and in-memory. It is useful for local
|
|
136
|
+
development, tests, and examples — it is not durable and does not
|
|
137
|
+
coordinate across processes or instances. A production deployment needs
|
|
138
|
+
an `ExecutionStore` backed by durable, atomic storage; see
|
|
139
|
+
[custom store](https://kaji.build/docs/guides/custom-store).
|
|
466
140
|
|
|
467
|
-
|
|
468
|
-
required only for catalog entries still marked experimental.
|
|
141
|
+
## What Kaji is not
|
|
469
142
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
import { z } from "zod";
|
|
477
|
-
|
|
478
|
-
registerTool(
|
|
479
|
-
toolSpecFromSchema("get_weather", "Look up weather", z.object({ city: z.string() }), "read"),
|
|
480
|
-
async (args, context) => ({
|
|
481
|
-
principalId: context.principalId,
|
|
482
|
-
city: args.city,
|
|
483
|
-
tempF: 68,
|
|
484
|
-
}),
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
const result = await executeTool(
|
|
488
|
-
"get_weather",
|
|
489
|
-
{ city: "Seattle" },
|
|
490
|
-
{
|
|
491
|
-
principalId: "user-1",
|
|
492
|
-
sessionId: "session-1",
|
|
493
|
-
turnId: "turn-1",
|
|
494
|
-
requestId: "request-1",
|
|
495
|
-
traceId: "trace-1",
|
|
496
|
-
toolCallId: "call-1",
|
|
497
|
-
idempotencyKey: "session-1:call-1",
|
|
498
|
-
signal: new AbortController().signal,
|
|
499
|
-
metadata: {},
|
|
500
|
-
},
|
|
501
|
-
);
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
## What's exported
|
|
505
|
-
|
|
506
|
-
| Export | What it is |
|
|
507
|
-
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
508
|
-
| `EventType`, `KajiEvent` | Event discriminants and Zod-validated event union |
|
|
509
|
-
| `EventStore`, `InMemoryEventStore` | Event log that is append-only while retained; explicit session purge is an optional lifecycle capability |
|
|
510
|
-
| `EventBus` | In-memory pub/sub per session |
|
|
511
|
-
| `replaySession`, `SessionManager`, session store types | Session projection and management |
|
|
512
|
-
| `registerTool`, `ToolRegistry`, `toolSpecFromSchema`, `executeTool`, `listToolSpecs` | Tool registry (global + scoped) |
|
|
513
|
-
| `ToolPolicy`, `ToolPlanner` | Allow/deny and approval-gated execution |
|
|
514
|
-
| `TypedApprovalHandler`, `cliApprovalHandler`, `EventApprovalHandler`, `AutoApprovalHandler` | Structured approval handlers: stdin, event-driven (publishes `TOOL_APPROVAL_REQUESTED` for a host UI to answer), and auto-decide by policy |
|
|
515
|
-
| `OpenAIProvider`, `AnthropicProvider` | LLM providers; Anthropic is experimental/WIP |
|
|
516
|
-
| `normalizeProviderError`, `NormalizedProviderError` | Redaction-safe semantic classification for Kaji provider errors |
|
|
517
|
-
| `openai`, `anthropic`, `kimi`, `gemini`, `openrouter` | One-line provider factories; only OpenAI is beta-supported |
|
|
518
|
-
| `getProvider`, `registerProvider` | Name-keyed provider registry, for host code that resolves a provider by config string |
|
|
519
|
-
| `generateText`, `streamText` | One-shot provider calls without a full `AgentRuntime`: a single request/response or stream, no event log |
|
|
520
|
-
| `AgentRuntime`, `AgentBuilder`, `CancellationToken` | ReAct loop and fluent builder |
|
|
521
|
-
| `EffectiveRuntimeLimits` | Immutable values returned by `AgentRuntime.effectiveLimits()` after overrides |
|
|
522
|
-
| `Integration`, `tool` | Integration helper for scoped tools |
|
|
523
|
-
| `EnvSecretSource` | Reads a named secret from `process.env`; the default `SecretSource` implementation |
|
|
524
|
-
|
|
525
|
-
Events use snake_case field names (`session_id`, `tool_name`) as the wire format
|
|
526
|
-
shared with the Python SDK.
|
|
527
|
-
|
|
528
|
-
## Python vs TypeScript parity
|
|
529
|
-
|
|
530
|
-
| Feature | Python SDK | TS SDK |
|
|
531
|
-
| ------------------------------------- | ---------------------- | --------------------------------------------------- |
|
|
532
|
-
| Event-sourced runtime | Yes | Yes |
|
|
533
|
-
| Tool registry + planner + policy | Yes | Yes |
|
|
534
|
-
| `AgentBuilder` + integrations | Yes | Yes |
|
|
535
|
-
| OpenAI provider | Yes | Yes |
|
|
536
|
-
| Anthropic provider (experimental/WIP) | Yes | Yes |
|
|
537
|
-
| Kimi / Gemini providers | Yes (experimental/WIP) | Yes (experimental/WIP, OpenAI-compatible factories) |
|
|
538
|
-
| Document RAG / vector store | Yes (non-MVP) | No |
|
|
539
|
-
| Tool retriever | Yes (non-MVP) | No |
|
|
540
|
-
| Text modality adapter | Yes (non-MVP) | No |
|
|
541
|
-
| Voice / TTS | Yes (non-MVP) | No |
|
|
542
|
-
| Redis realtime bus | Yes (non-MVP) | No (in-memory only) |
|
|
543
|
-
| CLI scaffold | Yes | Yes |
|
|
544
|
-
|
|
545
|
-
## Testing without API keys
|
|
546
|
-
|
|
547
|
-
Unit and integration tests mock the provider HTTP client -- no keys needed for
|
|
548
|
-
the default test suite:
|
|
549
|
-
|
|
550
|
-
```bash
|
|
551
|
-
bun run test
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
Live provider tests are opt-in and skip automatically when keys are absent.
|
|
555
|
-
OpenAI is the beta-supported live path; the Anthropic command remains a WIP
|
|
556
|
-
adapter check:
|
|
557
|
-
|
|
558
|
-
```bash
|
|
559
|
-
OPENAI_API_KEY=... bun run test:integration
|
|
560
|
-
OPENAI_API_KEY=... KAJI_LIVE_OPENAI_MODEL=gpt-5.4-mini \
|
|
561
|
-
bun run test:integration tests/integration/openai-tools.test.ts
|
|
562
|
-
ANTHROPIC_API_KEY=... bun run test:integration
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
`MockProvider` is a deterministic stub that exercises the full tool loop. It is
|
|
566
|
-
available from `@irogane/kaji/testing` for unit tests, not from the main package
|
|
567
|
-
entrypoint used to build real agents.
|
|
568
|
-
|
|
569
|
-
```ts
|
|
570
|
-
import { MockProvider } from "@irogane/kaji/testing";
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
## Development
|
|
574
|
-
|
|
575
|
-
```bash
|
|
576
|
-
# from the repository root
|
|
577
|
-
bun install
|
|
578
|
-
bun --filter kaji typecheck
|
|
579
|
-
bun --filter kaji format:check
|
|
580
|
-
bun --filter kaji test
|
|
581
|
-
bun --filter kaji build
|
|
582
|
-
```
|
|
143
|
+
Kaji does not provide an agent framework, model SDK, workflow engine,
|
|
144
|
+
MCP runtime, tool registry, identity provider, queue, scheduler, retry
|
|
145
|
+
engine, or event-sourcing system. Your application supplies identity,
|
|
146
|
+
authorization policy, and domain logic; Kaji only governs how a request
|
|
147
|
+
to run one action is validated, authorized, approved, executed once,
|
|
148
|
+
and recorded.
|
|
583
149
|
|
|
584
|
-
##
|
|
150
|
+
## Documentation
|
|
585
151
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
152
|
+
Full documentation, guides, and the API reference are at
|
|
153
|
+
[kaji.build](https://kaji.build). A complete runnable proof example
|
|
154
|
+
lives in [`examples/refund`](https://github.com/enkyuan/kaji/tree/main/examples/refund)
|
|
155
|
+
in the repository.
|
|
590
156
|
|
|
591
157
|
## License
|
|
592
158
|
|
|
593
|
-
|
|
594
|
-
[Functional Source License 1.1, ALv2 Future License](https://spdx.org/licenses/FSL-1.1-ALv2.html).
|
|
595
|
-
It permits internal commercial use, modification, and redistribution for
|
|
596
|
-
permitted purposes, but excludes competing commercial products and services;
|
|
597
|
-
each version becomes Apache-2.0 after two years. FSL is not an OSI-approved
|
|
598
|
-
open-source license.
|
|
159
|
+
FSL-1.1-ALv2. See [LICENSE](https://github.com/enkyuan/kaji/blob/main/LICENSE).
|