@kohala/devkit 0.1.0 → 0.1.2

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/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.
@@ -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`).
@@ -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
+ }