@kohala/devkit 0.1.0 → 0.1.1
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 +39 -0
- package/CODE_OF_CONDUCT.md +37 -0
- package/CONTRIBUTING.md +53 -0
- package/README.md +5 -3
- package/SECURITY.md +28 -0
- package/dist/cli/index.js +19 -8
- package/dist/cli/index.js.map +1 -1
- package/docs/DEPLOY.md +48 -0
- package/docs/EMULATOR.md +93 -0
- package/docs/MANIFEST.md +84 -0
- package/docs/MEMORY.md +81 -0
- package/docs/QUICKSTART.md +70 -0
- package/examples/llm-notes/README.md +18 -0
- package/examples/llm-notes/kohala.json +16 -0
- package/examples/llm-notes/skills/task.md +7 -0
- package/examples/rss-digest/README.md +13 -0
- package/examples/rss-digest/kohala.json +17 -0
- package/examples/rss-digest/skills/_tools.py +125 -0
- package/examples/rss-digest/skills/main.py +49 -0
- package/examples/weather-logger/README.md +12 -0
- package/examples/weather-logger/kohala.json +19 -0
- package/examples/weather-logger/skills/_tools.py +125 -0
- package/examples/weather-logger/skills/main.py +39 -0
- package/package.json +7 -2
package/docs/DEPLOY.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Deploying to kohala.ai
|
|
2
|
+
|
|
3
|
+
Everything in the devkit works without an account. Deploy is the one step
|
|
4
|
+
that needs one — it pushes your locally-validated agent to the hosted
|
|
5
|
+
platform, unchanged.
|
|
6
|
+
|
|
7
|
+
## Login
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
kohala login # paste your pk_ key from kohala.ai account settings
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The key is stored at `~/.kohala/credentials.json` with permissions `600`.
|
|
14
|
+
The `KOHALA_API_KEY` environment variable **always takes precedence** over
|
|
15
|
+
the file — use it in CI.
|
|
16
|
+
|
|
17
|
+
## Deploy
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
kohala deploy my-agent --dry-run # print exactly what would be sent
|
|
21
|
+
kohala deploy my-agent # do it
|
|
22
|
+
kohala deploy my-agent --run # ...and trigger a manual hosted run
|
|
23
|
+
kohala deploy my-agent --base-url https://staging.kohala.ai # non-prod target
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What deploy does
|
|
27
|
+
|
|
28
|
+
Deploy maps `kohala.json` onto the platform REST API, in order:
|
|
29
|
+
|
|
30
|
+
1. `POST /api/v1/agents` — create or update the agent. **Idempotent on the
|
|
31
|
+
agent name**: deploying twice updates, never duplicates.
|
|
32
|
+
2. `POST /api/v1/agents/:id/skills` — upload each skill script.
|
|
33
|
+
3. `PUT /api/v1/agents/:id/quota` — set the token caps.
|
|
34
|
+
4. (with `--run`) `POST /api/v1/agents/:id/agent-runs/manual` — trigger a
|
|
35
|
+
run and print its URL.
|
|
36
|
+
|
|
37
|
+
Deploy is **additive only** — it never deletes agents, skills, or memory
|
|
38
|
+
remotely. Removing a skill from kohala.json does not remove it from the
|
|
39
|
+
platform; do that in the Kohala dashboard.
|
|
40
|
+
|
|
41
|
+
## Errors you might see
|
|
42
|
+
|
|
43
|
+
- **401 Unauthorized** — the key was rejected. Re-run `kohala login` with a
|
|
44
|
+
fresh `pk_` key, or check `KOHALA_API_KEY`.
|
|
45
|
+
- **403 Forbidden** — your plan does not allow the operation. Check your
|
|
46
|
+
plan at kohala.ai.
|
|
47
|
+
|
|
48
|
+
Both messages tell you this directly; nothing is retried silently.
|
package/docs/EMULATOR.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# How the emulator works
|
|
2
|
+
|
|
3
|
+
`kohala run <agent> --local` executes one **shift** with the same enforcement
|
|
4
|
+
order the hosted platform uses. Nothing is mocked, nothing is billed.
|
|
5
|
+
|
|
6
|
+
## Enforcement order
|
|
7
|
+
|
|
8
|
+
1. **Admission (per-day cap).** Before any work, the emulator sums today's
|
|
9
|
+
(UTC) token usage from `.kohala/usage/<agent>.json`. If it has already
|
|
10
|
+
reached `caps.perDayTokens`, the shift is refused with
|
|
11
|
+
`PER_DAY_TOKEN_CAP` — exactly like the platform's admission check.
|
|
12
|
+
2. **Tool allowlist.** Every tool call — from a wrap-mode script over RPC or
|
|
13
|
+
from the llm-mode loop — is checked against `toolAllowlist`. Disallowed
|
|
14
|
+
calls fail with `TOOL_DENIED` and the denial is recorded in the trace.
|
|
15
|
+
3. **Per-run cap.** Before each LLM turn (an `llm.complete` call in wrap
|
|
16
|
+
mode, or a loop turn in llm mode), the emulator projects the turn's token
|
|
17
|
+
cost. If the projection would cross `caps.perRunTokens`, the run aborts
|
|
18
|
+
with `PER_RUN_TOKEN_CAP` instead of crossing it.
|
|
19
|
+
4. **Validators + repair loop.** After the output is produced, all
|
|
20
|
+
validators run. On failure, wrap mode re-runs the script with feedback,
|
|
21
|
+
at most **2** repair attempts (platform default), then the run is marked
|
|
22
|
+
`failed`.
|
|
23
|
+
|
|
24
|
+
## Token accounting — counted, never billed
|
|
25
|
+
|
|
26
|
+
Tokens are estimated at ~4 characters/token for projections; actual LLM
|
|
27
|
+
usage comes from the provider's response. Totals are written to the trace
|
|
28
|
+
(`tokens` events) and to the per-day ledger. **No money is involved
|
|
29
|
+
locally, ever.** The caps exist so your agent behaves identically when
|
|
30
|
+
deployed.
|
|
31
|
+
|
|
32
|
+
## The script boundary (wrap mode)
|
|
33
|
+
|
|
34
|
+
Skill scripts run as a separate Python process and talk to the emulator over
|
|
35
|
+
a loopback HTTP RPC endpoint — the same boundary shape the platform uses.
|
|
36
|
+
The emulator passes:
|
|
37
|
+
|
|
38
|
+
| Env var | Meaning |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `KOHALA_RPC_URL` | Loopback endpoint for tool calls (`skills/_tools.py` uses it) |
|
|
41
|
+
| `KOHALA_AGENT` | Agent name |
|
|
42
|
+
| `KOHALA_RUN_ID` | Unique shift id |
|
|
43
|
+
| `KOHALA_REPAIR_ATTEMPT` | `0` first try, `1`–`2` on repair attempts |
|
|
44
|
+
| `KOHALA_VALIDATOR_FEEDBACK` | Why validators failed last attempt |
|
|
45
|
+
|
|
46
|
+
The script's **stdout is the run output**; stderr passes through to your
|
|
47
|
+
terminal for debugging.
|
|
48
|
+
|
|
49
|
+
## llm mode
|
|
50
|
+
|
|
51
|
+
A real Anthropic tool-use loop with your own `ANTHROPIC_API_KEY`:
|
|
52
|
+
|
|
53
|
+
- `charter` → system prompt
|
|
54
|
+
- skill file contents → task context
|
|
55
|
+
- allowlisted tools → tool definitions (the model can't even see others)
|
|
56
|
+
- every tool invocation goes through the same dispatcher as wrap mode
|
|
57
|
+
|
|
58
|
+
**`ANTHROPIC_API_KEY` is required for `runtimeMode: "llm"`.** The loop uses
|
|
59
|
+
the Anthropic tool-use API. `GEMINI_API_KEY` enables `llm.complete` calls
|
|
60
|
+
inside wrap-mode skill scripts, but not the llm-mode tool-use loop.
|
|
61
|
+
|
|
62
|
+
If no Anthropic key is configured the run fails with a clear error message.
|
|
63
|
+
There is no mock fallback by design.
|
|
64
|
+
|
|
65
|
+
`kohala doctor` reports both cases separately so you can see exactly which
|
|
66
|
+
features your current environment supports.
|
|
67
|
+
|
|
68
|
+
## Models
|
|
69
|
+
|
|
70
|
+
The emulator defaults to the same models the hosted platform runs:
|
|
71
|
+
|
|
72
|
+
| Provider | Default model | Override env var |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| Anthropic | `claude-sonnet-4-6` | `ANTHROPIC_MODEL` |
|
|
75
|
+
| Gemini | `gemini-flash-latest` | `GEMINI_MODEL` |
|
|
76
|
+
|
|
77
|
+
Both can be overridden with `KOHALA_LLM_MODEL` (takes precedence over the
|
|
78
|
+
provider-specific vars). Example:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
ANTHROPIC_MODEL=claude-3-5-haiku-latest kohala run my-agent --local
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Because caps are token-based and consumption varies by model, keeping the
|
|
85
|
+
local default in sync with the hosted platform means your cap tuning carries
|
|
86
|
+
over when you deploy.
|
|
87
|
+
|
|
88
|
+
## The trace
|
|
89
|
+
|
|
90
|
+
Every shift appends JSONL events to `.kohala/trace/<agent>.jsonl`:
|
|
91
|
+
`run_started`, `tool_call` (with allow/deny and duration), `tokens`,
|
|
92
|
+
`validator_result`, `repair_attempt`, `run_finished`. Inspect with
|
|
93
|
+
`kohala trace <agent>` (`--follow`, `--json`).
|
package/docs/MANIFEST.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Manifest reference — kohala.json
|
|
2
|
+
|
|
3
|
+
`kohala.json` is the agent's single source of truth, and it maps 1:1 onto
|
|
4
|
+
platform fields. Deploying never re-interprets it — what you validate locally
|
|
5
|
+
is what goes live.
|
|
6
|
+
|
|
7
|
+
## Full example
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"name": "weather-logger",
|
|
12
|
+
"charter": "Fetch current weather once per shift and store it in memory.",
|
|
13
|
+
"toolAllowlist": ["s3.put", "s3.get", "s3.list", "http.post_json"],
|
|
14
|
+
"runtimeMode": "wrap",
|
|
15
|
+
"skills": { "collect": "main.py" },
|
|
16
|
+
"schedule": "0 * * * *",
|
|
17
|
+
"caps": {
|
|
18
|
+
"perRunTokens": 10000,
|
|
19
|
+
"perDayTokens": 50000,
|
|
20
|
+
"billingTokens": 1000000,
|
|
21
|
+
"billingPeriod": "month"
|
|
22
|
+
},
|
|
23
|
+
"validators": [
|
|
24
|
+
{ "type": "shape", "minBytes": 20 },
|
|
25
|
+
{ "type": "freshness", "asset": "weather/latest", "maxAgeHours": 2 },
|
|
26
|
+
{ "type": "invariant", "pattern": "temperature", "mustMatch": true }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Fields
|
|
32
|
+
|
|
33
|
+
| Field | Platform field | Meaning |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `name` | agent name | Identity. Deploy is idempotent on it. Letters, digits, `-`, `_`. |
|
|
36
|
+
| `charter` | `agentCharter` | The agent's mission. In llm mode this is the system prompt. |
|
|
37
|
+
| `toolAllowlist` | `agentToolAllowlist` | Exactly the tools the agent may call. No implicit grants. |
|
|
38
|
+
| `runtimeMode` | `agentRuntimeMode` | `"wrap"` or `"llm"` (see below). |
|
|
39
|
+
| `skills` | `agentSkills` | Map of skill name → script filename in `skills/`. |
|
|
40
|
+
| `schedule` | `agentScheduleCron` | Cron expression. Only used on deploy; local runs are manual. |
|
|
41
|
+
| `caps.perRunTokens` | `agentPerRunTokenCap` | Hard token ceiling per shift. |
|
|
42
|
+
| `caps.perDayTokens` | `agentPerDayTokenCap` | Cumulative ceiling per UTC day. |
|
|
43
|
+
| `caps.billingTokens` | `agentBillingCapTokens` | Billing-period cap. Ignored locally. |
|
|
44
|
+
| `caps.billingPeriod` | `agentBillingCapPeriod` | `"day"`, `"week"`, or `"month"`. Required with `billingTokens`. |
|
|
45
|
+
| `validators` | agent validators | Output checks (below). |
|
|
46
|
+
|
|
47
|
+
## Runtime modes
|
|
48
|
+
|
|
49
|
+
- **`wrap`** — the emulator executes the skill script directly (Python 3).
|
|
50
|
+
The script's **stdout is the run output** that validators evaluate; stderr
|
|
51
|
+
is for your debug logging. The script uses `skills/_tools.py` to call
|
|
52
|
+
tools over the loopback RPC boundary.
|
|
53
|
+
- **`llm`** — a real tool-use loop against your own `ANTHROPIC_API_KEY`. The
|
|
54
|
+
charter is the system prompt, the skill file's contents are the task
|
|
55
|
+
context, and allowlisted tools are exposed to the model. The final text
|
|
56
|
+
reply is the run output.
|
|
57
|
+
|
|
58
|
+
## Tools
|
|
59
|
+
|
|
60
|
+
`s3.put`, `s3.get`, `s3.list`, `s3.delete`, `http.post_json`,
|
|
61
|
+
`llm.complete`, `notify.send`, `metrics.record` — identical names and
|
|
62
|
+
semantics locally and hosted. Locally, `notify.send` and `metrics.record`
|
|
63
|
+
land in the trace instead of sending anything.
|
|
64
|
+
|
|
65
|
+
## Validators
|
|
66
|
+
|
|
67
|
+
- `{"type": "shape", "minBytes": n}` — output must exist and be ≥ n bytes.
|
|
68
|
+
- `{"type": "freshness", "asset": "key", "maxAgeHours": h}` — the memory
|
|
69
|
+
asset at `key` must have been updated within the last `h` hours.
|
|
70
|
+
- `{"type": "invariant", "pattern": "regex", "mustMatch": true|false}` —
|
|
71
|
+
output must match (or must not match) the regex.
|
|
72
|
+
|
|
73
|
+
On failure, wrap mode re-runs the script up to **2** more times with
|
|
74
|
+
`KOHALA_REPAIR_ATTEMPT` and `KOHALA_VALIDATOR_FEEDBACK` set — the platform's
|
|
75
|
+
bounded repair loop.
|
|
76
|
+
|
|
77
|
+
## Validation errors
|
|
78
|
+
|
|
79
|
+
`kohala validate <agent>` prints every problem with a fix hint, e.g.:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
kohala.json failed validation
|
|
83
|
+
• caps.perRunTokens: Expected number, received string (hint: set caps.perRunTokens and caps.perDayTokens as positive integers)
|
|
84
|
+
```
|
package/docs/MEMORY.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Memory & the MCP server
|
|
2
|
+
|
|
3
|
+
Agent memory locally has the same surface as the hosted platform:
|
|
4
|
+
|
|
5
|
+
- `s3.put(key, body, category?)` — store under a logical key. Category
|
|
6
|
+
defaults to `"agentoutput"` (run results).
|
|
7
|
+
- `s3.get(keyOrId)` — resolve by active logical key first, then record id.
|
|
8
|
+
- `s3.list(prefix?, limit?)` — active assets, newest first.
|
|
9
|
+
- `s3.delete(keyOrId)` — remove the body + deactivate the index entry
|
|
10
|
+
(soft delete).
|
|
11
|
+
|
|
12
|
+
Logical keys are unique among active assets: putting to an existing key
|
|
13
|
+
updates it in place.
|
|
14
|
+
|
|
15
|
+
## Backends
|
|
16
|
+
|
|
17
|
+
### file (default)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
.kohala/memory/<agent>/
|
|
21
|
+
├── index.json # asset index: key, category, timestamps, active flag
|
|
22
|
+
└── bodies/<id> # raw body bytes, one file per asset
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The index is rewritten atomically (temp file + rename) on every mutation.
|
|
26
|
+
|
|
27
|
+
### postgres
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
kohala memory serve --backend postgres --url postgres://... # or DATABASE_URL
|
|
31
|
+
kohala run my-agent --local --backend postgres
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
One table, `kohala_memory`, created automatically on first connect. Bodies
|
|
35
|
+
are stored as `bytea` in the same row — a deliberate design decision so any
|
|
36
|
+
plain Postgres URL works with no filesystem coupling. Requires the optional
|
|
37
|
+
`pg` package (`npm install pg`).
|
|
38
|
+
|
|
39
|
+
## The MCP server
|
|
40
|
+
|
|
41
|
+
`kohala memory serve` exposes memory over the
|
|
42
|
+
[Model Context Protocol](https://modelcontextprotocol.io) so any MCP client
|
|
43
|
+
(Claude Desktop, MCP Inspector, your own tools) can read and write agent
|
|
44
|
+
memory with the platform's exact tool names.
|
|
45
|
+
|
|
46
|
+
The command must know which agent's memory to scope to. Pass `--agent
|
|
47
|
+
<name>` explicitly, **or** run it from inside an agent directory (one that
|
|
48
|
+
contains `kohala.json`) and it will read the agent name automatically:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Option 1: explicit agent name — run from anywhere
|
|
52
|
+
kohala memory serve --agent my-agent
|
|
53
|
+
|
|
54
|
+
# Option 2: run from inside the agent directory
|
|
55
|
+
cd my-agent
|
|
56
|
+
kohala memory serve # reads agent name from ./kohala.json
|
|
57
|
+
|
|
58
|
+
# Streamable HTTP — for MCP Inspector etc.
|
|
59
|
+
kohala memory serve --agent my-agent --http --port 8787
|
|
60
|
+
# endpoint: http://127.0.0.1:8787/mcp
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Tools: `s3.put`, `s3.get`, `s3.list`, `s3.delete`.
|
|
64
|
+
Resource: `memory://index` — a JSON listing of all active assets.
|
|
65
|
+
|
|
66
|
+
Claude Desktop config example:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"kohala-memory": {
|
|
72
|
+
"command": "kohala",
|
|
73
|
+
"args": ["memory", "serve", "--agent", "my-agent"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Run it from the directory that contains your `.kohala/` folder (or from
|
|
80
|
+
inside the agent directory — the server resolves the agent name from
|
|
81
|
+
kohala.json and uses the parent directory as the workspace root).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
From zero to a running agent in under five minutes. No account needed.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @kohala/devkit
|
|
9
|
+
kohala --version
|
|
10
|
+
kohala doctor # checks Node, Python, keys
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requirements: Node.js ≥ 20 and Python 3 on your PATH.
|
|
14
|
+
|
|
15
|
+
## Create an agent
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
kohala init my-agent
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This scaffolds:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
my-agent/
|
|
25
|
+
├── kohala.json # manifest: charter, tools, caps, validators
|
|
26
|
+
├── README.md
|
|
27
|
+
└── skills/
|
|
28
|
+
├── main.py # the skill — its stdout is the run output
|
|
29
|
+
└── _tools.py # the local tool SDK (stdlib-only, don't edit)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Run it
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
kohala validate my-agent
|
|
36
|
+
kohala run my-agent --local
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You'll see the run status, token count, validator results, and the output.
|
|
40
|
+
The emulator enforces the platform's exact rules — per-day admission, tool
|
|
41
|
+
allowlist, per-run token caps, validators with a bounded repair loop — but
|
|
42
|
+
**never bills anything**.
|
|
43
|
+
|
|
44
|
+
## Inspect the audit trail
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
kohala trace my-agent # pretty view
|
|
48
|
+
kohala trace my-agent --follow # tail it live
|
|
49
|
+
kohala trace my-agent --json # raw JSONL
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every tool call, token increment, validator result, and repair attempt is in
|
|
53
|
+
there.
|
|
54
|
+
|
|
55
|
+
## Iterate
|
|
56
|
+
|
|
57
|
+
Edit `my-agent/skills/main.py` and `my-agent/kohala.json`, then run again.
|
|
58
|
+
Try removing a tool from `toolAllowlist` and watch the call fail loudly with
|
|
59
|
+
`TOOL_DENIED` — that is exactly what the platform would do.
|
|
60
|
+
|
|
61
|
+
## Go live (optional)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
kohala login # paste your pk_ key from kohala.ai
|
|
65
|
+
kohala deploy my-agent --dry-run # see exactly what would be sent
|
|
66
|
+
kohala deploy my-agent --run # deploy + trigger a hosted run
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Next: [Manifest reference](MANIFEST.md) · [How the emulator works](EMULATOR.md)
|
|
70
|
+
· [Memory & MCP](MEMORY.md) · [Deploying](DEPLOY.md)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# llm-notes example
|
|
2
|
+
|
|
3
|
+
`runtimeMode: "llm"` — a real tool-use loop against your own
|
|
4
|
+
`ANTHROPIC_API_KEY`. The charter is the system prompt, `skills/task.md` is
|
|
5
|
+
the task context, and the allowlisted tools (`s3.put`, `s3.get`, `s3.list`,
|
|
6
|
+
`notify.send`) are exposed to the model. Every tool call the model makes goes
|
|
7
|
+
through the same allowlist + trace + token accounting as wrap mode.
|
|
8
|
+
|
|
9
|
+
Before each turn the emulator projects the request's token cost against
|
|
10
|
+
`caps.perRunTokens` and aborts with `PER_RUN_TOKEN_CAP` rather than crossing
|
|
11
|
+
it.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
15
|
+
cd examples
|
|
16
|
+
kohala run llm-notes --local
|
|
17
|
+
kohala trace llm-notes
|
|
18
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "llm-notes",
|
|
3
|
+
"charter": "You are a note-keeping agent. Each shift, review the notes already in memory (s3.list, s3.get), write one new short observation about what you find under notes/<n>, and finish with a one-line summary.",
|
|
4
|
+
"toolAllowlist": ["s3.put", "s3.get", "s3.list", "notify.send"],
|
|
5
|
+
"runtimeMode": "llm",
|
|
6
|
+
"skills": {
|
|
7
|
+
"observe": "task.md"
|
|
8
|
+
},
|
|
9
|
+
"caps": {
|
|
10
|
+
"perRunTokens": 30000,
|
|
11
|
+
"perDayTokens": 120000
|
|
12
|
+
},
|
|
13
|
+
"validators": [
|
|
14
|
+
{ "type": "shape", "minBytes": 10 }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Task
|
|
2
|
+
|
|
3
|
+
1. List what is currently stored under the `notes/` prefix.
|
|
4
|
+
2. Store one new note at `notes/<next-number>` — a single short sentence
|
|
5
|
+
observing something about the existing notes (or, if there are none, a
|
|
6
|
+
first observation about starting fresh).
|
|
7
|
+
3. Reply with a one-line summary of what you stored.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# rss-digest example
|
|
2
|
+
|
|
3
|
+
Wrap-mode agent that fetches Hacker News front-page headlines and summarizes
|
|
4
|
+
them with `llm.complete` — using **your own** `ANTHROPIC_API_KEY` (or
|
|
5
|
+
`GEMINI_API_KEY`). Without a key the run fails loudly with `NO_LLM_KEY`;
|
|
6
|
+
there is no mock fallback.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
10
|
+
cd examples
|
|
11
|
+
kohala run rss-digest --local
|
|
12
|
+
kohala trace rss-digest # note the `tokens` events — counted, never billed
|
|
13
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rss-digest",
|
|
3
|
+
"charter": "Summarize the newest items from a feed into a short digest and store it under digest/latest.",
|
|
4
|
+
"toolAllowlist": ["s3.put", "s3.get", "llm.complete", "notify.send"],
|
|
5
|
+
"runtimeMode": "wrap",
|
|
6
|
+
"skills": {
|
|
7
|
+
"digest": "main.py"
|
|
8
|
+
},
|
|
9
|
+
"caps": {
|
|
10
|
+
"perRunTokens": 20000,
|
|
11
|
+
"perDayTokens": 80000
|
|
12
|
+
},
|
|
13
|
+
"validators": [
|
|
14
|
+
{ "type": "shape", "minBytes": 40 },
|
|
15
|
+
{ "type": "freshness", "asset": "digest/latest", "maxAgeHours": 25 }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Kohala tool SDK (local twin).
|
|
2
|
+
|
|
3
|
+
Your skill script talks to the Kohala runtime through these helpers. Locally
|
|
4
|
+
they call the emulator over a loopback RPC endpoint; on the hosted platform
|
|
5
|
+
the same functions talk to the real runtime. Your script does not change.
|
|
6
|
+
|
|
7
|
+
Uses only the Python standard library — no pip installs needed.
|
|
8
|
+
|
|
9
|
+
Available tools (each must also be listed in kohala.json -> toolAllowlist):
|
|
10
|
+
|
|
11
|
+
s3_put(key, body, category=None) store text in agent memory
|
|
12
|
+
s3_get(key_or_id) fetch a memory asset
|
|
13
|
+
s3_list(prefix=None, limit=None) list active memory assets
|
|
14
|
+
s3_delete(key_or_id) remove + deactivate an asset
|
|
15
|
+
http_post_json(url, body, headers=None) POST JSON to an external API
|
|
16
|
+
llm_complete(prompt, model=None) complete text with YOUR OWN LLM key
|
|
17
|
+
notify_send(channel, message) send a notification (trace, locally)
|
|
18
|
+
metrics_record(name, value, tags=None) record a metric (trace, locally)
|
|
19
|
+
|
|
20
|
+
Every helper raises KohalaToolError on failure — including TOOL_DENIED when
|
|
21
|
+
the tool is not in your allowlist, and PER_RUN_TOKEN_CAP when an LLM call
|
|
22
|
+
would cross your per-run token cap. Errors are loud on purpose.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import urllib.request
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class KohalaToolError(Exception):
|
|
31
|
+
"""A tool call failed. `code` is the platform's machine-readable code."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, code, message):
|
|
34
|
+
super().__init__(f"{code}: {message}")
|
|
35
|
+
self.code = code
|
|
36
|
+
self.message = message
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _rpc(tool, args):
|
|
40
|
+
rpc_url = os.environ.get("KOHALA_RPC_URL")
|
|
41
|
+
if not rpc_url:
|
|
42
|
+
raise KohalaToolError(
|
|
43
|
+
"NO_RUNTIME",
|
|
44
|
+
"KOHALA_RPC_URL is not set. Run this script via `kohala run <agent> --local`, "
|
|
45
|
+
"not directly with python.",
|
|
46
|
+
)
|
|
47
|
+
payload = json.dumps({"tool": tool, "args": args}).encode("utf-8")
|
|
48
|
+
request = urllib.request.Request(
|
|
49
|
+
rpc_url,
|
|
50
|
+
data=payload,
|
|
51
|
+
headers={"Content-Type": "application/json"},
|
|
52
|
+
method="POST",
|
|
53
|
+
)
|
|
54
|
+
with urllib.request.urlopen(request) as response:
|
|
55
|
+
body = json.loads(response.read().decode("utf-8"))
|
|
56
|
+
if not body.get("ok"):
|
|
57
|
+
error = body.get("error") or {}
|
|
58
|
+
raise KohalaToolError(error.get("code", "UNKNOWN"), error.get("message", "tool call failed"))
|
|
59
|
+
return body.get("result")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def s3_put(key, body, category=None):
|
|
63
|
+
args = {"key": key, "body": body}
|
|
64
|
+
if category is not None:
|
|
65
|
+
args["category"] = category
|
|
66
|
+
return _rpc("s3.put", args)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def s3_get(key_or_id):
|
|
70
|
+
return _rpc("s3.get", {"keyOrId": key_or_id})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def s3_list(prefix=None, limit=None):
|
|
74
|
+
args = {}
|
|
75
|
+
if prefix is not None:
|
|
76
|
+
args["prefix"] = prefix
|
|
77
|
+
if limit is not None:
|
|
78
|
+
args["limit"] = limit
|
|
79
|
+
return _rpc("s3.list", args)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def s3_delete(key_or_id):
|
|
83
|
+
return _rpc("s3.delete", {"keyOrId": key_or_id})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def http_post_json(url, body, headers=None):
|
|
87
|
+
args = {"url": url, "body": body}
|
|
88
|
+
if headers is not None:
|
|
89
|
+
args["headers"] = headers
|
|
90
|
+
return _rpc("http.post_json", args)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def llm_complete(prompt, model=None):
|
|
94
|
+
args = {"prompt": prompt}
|
|
95
|
+
if model is not None:
|
|
96
|
+
args["model"] = model
|
|
97
|
+
return _rpc("llm.complete", args)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def notify_send(channel, message):
|
|
101
|
+
return _rpc("notify.send", {"channel": channel, "message": message})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def metrics_record(name, value, tags=None):
|
|
105
|
+
args = {"name": name, "value": value}
|
|
106
|
+
if tags is not None:
|
|
107
|
+
args["tags"] = tags
|
|
108
|
+
return _rpc("metrics.record", args)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run_context():
|
|
112
|
+
"""Info about the current shift, including repair-loop state.
|
|
113
|
+
|
|
114
|
+
Returns a dict with:
|
|
115
|
+
agent the agent name
|
|
116
|
+
run_id unique id of this shift
|
|
117
|
+
repair_attempt 0 on the first try, 1..2 on repair attempts
|
|
118
|
+
validator_feedback why validators failed last attempt (empty on first try)
|
|
119
|
+
"""
|
|
120
|
+
return {
|
|
121
|
+
"agent": os.environ.get("KOHALA_AGENT", ""),
|
|
122
|
+
"run_id": os.environ.get("KOHALA_RUN_ID", ""),
|
|
123
|
+
"repair_attempt": int(os.environ.get("KOHALA_REPAIR_ATTEMPT", "0")),
|
|
124
|
+
"validator_feedback": os.environ.get("KOHALA_VALIDATOR_FEEDBACK", ""),
|
|
125
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""rss-digest — summarize headlines with YOUR OWN LLM key.
|
|
2
|
+
|
|
3
|
+
Demonstrates: llm.complete (requires ANTHROPIC_API_KEY or GEMINI_API_KEY in
|
|
4
|
+
your environment — the devkit never mocks completions), plus the per-run
|
|
5
|
+
token cap: if the call would cross caps.perRunTokens it fails loudly with
|
|
6
|
+
PER_RUN_TOKEN_CAP.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.request
|
|
11
|
+
import xml.etree.ElementTree as ET
|
|
12
|
+
|
|
13
|
+
from _tools import s3_put, llm_complete, notify_send, KohalaToolError
|
|
14
|
+
|
|
15
|
+
FEED_URL = "https://hnrss.org/frontpage"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fetch_titles(limit=8):
|
|
19
|
+
with urllib.request.urlopen(FEED_URL, timeout=30) as response:
|
|
20
|
+
tree = ET.parse(response)
|
|
21
|
+
titles = [item.findtext("title") or "" for item in tree.iter("item")]
|
|
22
|
+
return [title for title in titles if title][:limit]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
titles = fetch_titles()
|
|
27
|
+
if not titles:
|
|
28
|
+
print("feed returned no items", file=sys.stderr)
|
|
29
|
+
raise SystemExit(1)
|
|
30
|
+
|
|
31
|
+
prompt = (
|
|
32
|
+
"Summarize these headlines into a 3-sentence digest for a busy reader:\n- "
|
|
33
|
+
+ "\n- ".join(titles)
|
|
34
|
+
)
|
|
35
|
+
try:
|
|
36
|
+
completion = llm_complete(prompt)
|
|
37
|
+
except KohalaToolError as error:
|
|
38
|
+
# NO_LLM_KEY or PER_RUN_TOKEN_CAP — both are loud by design.
|
|
39
|
+
print(f"llm.complete failed: {error}", file=sys.stderr)
|
|
40
|
+
raise SystemExit(1)
|
|
41
|
+
|
|
42
|
+
digest = completion["text"].strip()
|
|
43
|
+
s3_put("digest/latest", digest)
|
|
44
|
+
notify_send("dev", "digest updated")
|
|
45
|
+
print(digest)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# weather-logger example
|
|
2
|
+
|
|
3
|
+
Wrap-mode agent that fetches current weather from Open-Meteo (no API key
|
|
4
|
+
needed) and stores it in memory. Shows `http.post_json`, `s3.put`, a
|
|
5
|
+
`freshness` validator, and an `invariant` validator.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd examples
|
|
9
|
+
kohala validate weather-logger
|
|
10
|
+
kohala run weather-logger --local
|
|
11
|
+
kohala trace weather-logger
|
|
12
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "weather-logger",
|
|
3
|
+
"charter": "Fetch current weather conditions once per shift and store them in memory under weather/latest.",
|
|
4
|
+
"toolAllowlist": ["s3.put", "s3.get", "s3.list", "http.post_json", "metrics.record"],
|
|
5
|
+
"runtimeMode": "wrap",
|
|
6
|
+
"skills": {
|
|
7
|
+
"collect": "main.py"
|
|
8
|
+
},
|
|
9
|
+
"schedule": "0 * * * *",
|
|
10
|
+
"caps": {
|
|
11
|
+
"perRunTokens": 10000,
|
|
12
|
+
"perDayTokens": 50000
|
|
13
|
+
},
|
|
14
|
+
"validators": [
|
|
15
|
+
{ "type": "shape", "minBytes": 20 },
|
|
16
|
+
{ "type": "freshness", "asset": "weather/latest", "maxAgeHours": 2 },
|
|
17
|
+
{ "type": "invariant", "pattern": "temperature", "mustMatch": true }
|
|
18
|
+
]
|
|
19
|
+
}
|