@kohala/devkit 0.1.6 → 0.2.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/docs/DEPLOY.md CHANGED
@@ -21,6 +21,7 @@ kohala deploy my-agent --dry-run # print exactly what would be sent
21
21
  kohala deploy my-agent # do it
22
22
  kohala deploy my-agent --run # ...and trigger a manual hosted run
23
23
  kohala deploy my-agent --base-url https://staging.kohala.ai # non-prod target
24
+ kohala deploy my-agent --allow-unknown-packages # skip the local npm allowlist check
24
25
  ```
25
26
 
26
27
  ## What deploy does
@@ -34,6 +35,14 @@ Deploy maps `kohala.json` onto the platform REST API, in order:
34
35
  4. (with `--run`) `POST /api/v1/agents/:id/agent-runs/manual` — trigger a
35
36
  run and print its URL.
36
37
 
38
+ Each skill upload carries the script's runtime. Python skills are sent
39
+ exactly as they always were; a TypeScript/JavaScript skill (`.ts`, `.js`, …)
40
+ also sends `runtimeLanguage: "node"` and the manifest's `dependencies` as
41
+ `scriptDependencies`. That is what makes the platform run its acceptance
42
+ checks **when the script is attached** — TypeScript compile, security scan,
43
+ and the npm allowlist — instead of letting the script fail on its first
44
+ hosted run. The CLI prints the detected language for every skill it uploads.
45
+
37
46
  Deploy is **additive only** — it never deletes agents, skills, or memory
38
47
  remotely. Removing a skill from kohala.json does not remove it from the
39
48
  platform; do that in the Kohala dashboard.
@@ -44,5 +53,22 @@ platform; do that in the Kohala dashboard.
44
53
  fresh `pk_` key, or check `KOHALA_API_KEY`.
45
54
  - **403 Forbidden** — your plan does not allow the operation. Check your
46
55
  plan at kohala.ai.
56
+ - **unsupported npm dependencies for framework "custom": …** — a package in
57
+ `dependencies` is not one the platform installs. The CLI reports this
58
+ before sending anything; the same message comes back from the platform if
59
+ you deploy with `--allow-unknown-packages`. See the allowlist in
60
+ [MANIFEST.md](MANIFEST.md#script-languages).
61
+ - **Script rejected: TypeScript compile failed / security issue(s) found** —
62
+ the platform checks a TypeScript/JavaScript script when it is attached,
63
+ before storing it. Nothing is deployed; fix the reported lines and
64
+ redeploy.
65
+ - **manual run not started: agent not enabled** — newly created agents start
66
+ disabled. Enable the agent in the kohala.ai dashboard, then re-run
67
+ `kohala deploy <agent> --run`. Enabling survives redeploys — the idempotent
68
+ upsert does not reset it.
69
+ - **manual run not started: nothing to run** — the platform only executes
70
+ scripts bound to an active cron schedule. Add `"schedule": "..."` (a
71
+ 5-field cron expression) to `kohala.json` and redeploy: deploy binds every
72
+ skill script to the schedule, which also makes manual runs work.
47
73
 
48
74
  Both messages tell you this directly; nothing is retried silently.
package/docs/MANIFEST.md CHANGED
@@ -36,7 +36,8 @@ is what goes live.
36
36
  | `charter` | `agentCharter` | The agent's mission. In llm mode this is the system prompt. |
37
37
  | `toolAllowlist` | `agentToolAllowlist` | Exactly the tools the agent may call. No implicit grants. |
38
38
  | `runtimeMode` | `agentRuntimeMode` | `"wrap"` or `"llm"` (see below). |
39
- | `skills` | `agentSkills` | Map of skill name → script filename in `skills/`. |
39
+ | `skills` | `agentSkills` | Map of skill name → script filename in `skills/`. The extension picks the runtime (see below). |
40
+ | `dependencies` | `scriptDependencies` | npm packages the TypeScript/JavaScript skills import. Allowlisted packages only; omit for Python agents. |
40
41
  | `schedule` | `agentScheduleCron` + `agentScheduleEnabled` + `agentScheduleEntries` | Cron expression. Only used on deploy; local runs are manual. After uploading the skills, deploy PATCHes the schedule (the platform ignores schedule fields when updating an existing agent by name) **and binds every skill script to the cron via `agentScheduleEntries`** — without the binding the platform has nothing to run (409 `nothing_to_run`). The round-trip is verified. |
41
42
  | `caps.perRunTokens` | `agentPerRunTokenCap` | Hard token ceiling per shift. |
42
43
  | `caps.perDayTokens` | `agentPerDayTokenCap` | Cumulative ceiling per UTC day. |
@@ -44,6 +45,49 @@ is what goes live.
44
45
  | `caps.billingPeriod` | `agentBillingCapPeriod` | `"day"`, `"week"`, or `"month"`. Required with `billingTokens`. |
45
46
  | `validators` | agent validators | Output checks (below). |
46
47
 
48
+ ## Script languages
49
+
50
+ A skill script can be Python or TypeScript/JavaScript. **The file extension
51
+ decides which runtime the platform executes it in** — the same rule the
52
+ platform applies, so what `kohala validate` reports is what runs hosted:
53
+
54
+ | Extension | Runtime |
55
+ | --- | --- |
56
+ | `.py` | Python |
57
+ | `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs` | TypeScript/JavaScript |
58
+
59
+ Anything else is refused by `kohala validate` — the platform would store the
60
+ skill and never run it.
61
+
62
+ ```json
63
+ {
64
+ "skills": { "collect": "main.ts" },
65
+ "dependencies": ["zod", "date-fns"]
66
+ }
67
+ ```
68
+
69
+ `dependencies` lists the npm packages your TypeScript skills import beyond
70
+ Node's standard library, and is sent with the script on deploy. Only packages
71
+ the platform pre-installs are accepted:
72
+
73
+ `@ai-sdk/anthropic`, `@ai-sdk/openai`, `@anthropic-ai/sdk`, `ai`, `cheerio`,
74
+ `date-fns`, `js-yaml`, `openai`, `zod`
75
+
76
+ HTTP clients (`axios`, `node-fetch`, `undici`, …) are deliberately absent:
77
+ the hosted runtime meters egress through the global `fetch`. `kohala
78
+ validate` and `kohala deploy` refuse anything off the list **before** the
79
+ deploy request, with the platform's own message; `--allow-unknown-packages`
80
+ skips the local check if the CLI's snapshot is behind (the platform still
81
+ enforces the live list).
82
+
83
+ `dependencies` is npm-only. Python skills cannot install pip packages from
84
+ the CLI, so declaring packages without a `.ts`/`.js` skill is a validation
85
+ error rather than a silently ignored field.
86
+
87
+ **Local runs:** `kohala run --local` executes Python skills only. A
88
+ TypeScript skill deploys and runs hosted; locally the CLI says so instead of
89
+ handing the file to `python3`.
90
+
47
91
  ## Runtime modes
48
92
 
49
93
  - **`wrap`** — the emulator executes the skill script directly (Python 3).
@@ -10,12 +10,14 @@ kohala --version
10
10
  kohala doctor # checks Node, Python, keys
11
11
  ```
12
12
 
13
- Requirements: Node.js ≥ 20 and Python 3 on your PATH.
13
+ Requirements: Node.js ≥ 20. Python 3 is also required for Python agents.
14
14
 
15
15
  ## Create an agent
16
16
 
17
17
  ```bash
18
18
  kohala init my-agent
19
+ # Or scaffold TypeScript:
20
+ kohala init my-ts-agent --language ts
19
21
  ```
20
22
 
21
23
  This scaffolds:
@@ -29,6 +31,10 @@ my-agent/
29
31
  └── _tools.py # the local tool SDK (stdlib-only, don't edit)
30
32
  ```
31
33
 
34
+ The TypeScript scaffold uses `skills/main.ts` and `skills/_tools.ts`, and also
35
+ includes a strict `tsconfig.json` plus `package.json` with its development
36
+ dependencies declared.
37
+
32
38
  ## Run it
33
39
 
34
40
  ```bash
@@ -54,7 +60,7 @@ there.
54
60
 
55
61
  ## Iterate
56
62
 
57
- Edit `my-agent/skills/main.py` and `my-agent/kohala.json`, then run again.
63
+ Edit `my-agent/skills/main.py` (or `main.ts`) and `my-agent/kohala.json`, then run again.
58
64
  Try removing a tool from `toolAllowlist` and watch the call fail loudly with
59
65
  `TOOL_DENIED` — that is exactly what the platform would do.
60
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kohala/devkit",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Open-source CLI, local agent emulator, and open MCP memory server for Kohala agents. Build and run agents entirely on your own machine — then push the same agent to Kohala when you want it hosted.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -68,6 +68,7 @@
68
68
  "execa": "^9.6.1",
69
69
  "ora": "^9.4.1",
70
70
  "picocolors": "^1.1.1",
71
+ "tsx": "^4.21.0",
71
72
  "zod": "^3.25.76"
72
73
  },
73
74
  "devDependencies": {
@@ -0,0 +1,15 @@
1
+ # {{AGENT_NAME}}
2
+
3
+ A Python Kohala agent scaffolded by `kohala init`.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ kohala validate {{AGENT_NAME}}
9
+ kohala run {{AGENT_NAME}} --local
10
+ kohala trace {{AGENT_NAME}}
11
+ kohala deploy {{AGENT_NAME}} --dry-run
12
+ ```
13
+
14
+ Run these from the directory containing the `{{AGENT_NAME}}/` folder. Edit
15
+ `skills/main.py`; use `skills/_tools.py` for governed platform tool calls.
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "{{AGENT_NAME}}",
3
+ "charter": "Collect one interesting fact per shift and store it in memory. Keep the output short and factual.",
4
+ "toolAllowlist": ["s3.put", "s3.get", "s3.list", "notify.send", "metrics.record"],
5
+ "runtimeMode": "wrap",
6
+ "skills": {
7
+ "main": "main.py"
8
+ },
9
+ "caps": {
10
+ "perRunTokens": 20000,
11
+ "perDayTokens": 100000
12
+ },
13
+ "validators": [
14
+ { "type": "shape", "minBytes": 10 },
15
+ { "type": "freshness", "asset": "{{AGENT_NAME}}/latest", "maxAgeHours": 24 }
16
+ ]
17
+ }
@@ -0,0 +1,71 @@
1
+ """Kohala tool SDK. Uses the loopback runtime locally and hosted."""
2
+
3
+ import json
4
+ import os
5
+ import urllib.request
6
+
7
+
8
+ class KohalaToolError(Exception):
9
+ def __init__(self, code, message):
10
+ super().__init__(f"{code}: {message}")
11
+ self.code = code
12
+ self.message = message
13
+
14
+
15
+ def _rpc(tool, args):
16
+ rpc_url = os.environ.get("KOHALA_RPC_URL")
17
+ if not rpc_url:
18
+ raise KohalaToolError("NO_RUNTIME", "KOHALA_RPC_URL is not set. Run via `kohala run <agent> --local`.")
19
+ request = urllib.request.Request(
20
+ rpc_url,
21
+ data=json.dumps({"tool": tool, "args": args}).encode("utf-8"),
22
+ headers={"Content-Type": "application/json"},
23
+ method="POST",
24
+ )
25
+ with urllib.request.urlopen(request) as response:
26
+ body = json.loads(response.read().decode("utf-8"))
27
+ if not body.get("ok"):
28
+ error = body.get("error") or {}
29
+ raise KohalaToolError(error.get("code", "UNKNOWN"), error.get("message", "tool call failed"))
30
+ return body.get("result")
31
+
32
+
33
+ def s3_put(key, body, category=None):
34
+ return _rpc("s3.put", {"key": key, "body": body, **({"category": category} if category is not None else {})})
35
+
36
+
37
+ def s3_get(key_or_id):
38
+ return _rpc("s3.get", {"keyOrId": key_or_id})
39
+
40
+
41
+ def s3_list(prefix=None, limit=None):
42
+ return _rpc("s3.list", {**({"prefix": prefix} if prefix is not None else {}), **({"limit": limit} if limit is not None else {})})
43
+
44
+
45
+ def s3_delete(key_or_id):
46
+ return _rpc("s3.delete", {"keyOrId": key_or_id})
47
+
48
+
49
+ def http_post_json(url, body, headers=None):
50
+ return _rpc("http.post_json", {"url": url, "body": body, **({"headers": headers} if headers is not None else {})})
51
+
52
+
53
+ def llm_complete(prompt, model=None):
54
+ return _rpc("llm.complete", {"prompt": prompt, **({"model": model} if model is not None else {})})
55
+
56
+
57
+ def notify_send(channel, message):
58
+ return _rpc("notify.send", {"channel": channel, "message": message})
59
+
60
+
61
+ def metrics_record(name, value, tags=None):
62
+ return _rpc("metrics.record", {"name": name, "value": value, **({"tags": tags} if tags is not None else {})})
63
+
64
+
65
+ def run_context():
66
+ return {
67
+ "agent": os.environ.get("KOHALA_AGENT", ""),
68
+ "run_id": os.environ.get("KOHALA_RUN_ID", ""),
69
+ "repair_attempt": int(os.environ.get("KOHALA_REPAIR_ATTEMPT", "0")),
70
+ "validator_feedback": os.environ.get("KOHALA_VALIDATOR_FEEDBACK", ""),
71
+ }
@@ -0,0 +1,25 @@
1
+ """{{AGENT_NAME}} — main skill."""
2
+
3
+ import sys
4
+ import time
5
+
6
+ from _tools import s3_put, s3_list, notify_send, metrics_record, run_context
7
+
8
+
9
+ def main():
10
+ context = run_context()
11
+ if context["repair_attempt"] > 0:
12
+ print(f"repair attempt {context['repair_attempt']}: {context['validator_feedback']}",
13
+ file=sys.stderr)
14
+
15
+ fact = f"Shift ran at {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}."
16
+ s3_put("{{AGENT_NAME}}/latest", fact)
17
+ notify_send("dev", "shift completed")
18
+ metrics_record("facts_stored", 1)
19
+ existing = s3_list(prefix="{{AGENT_NAME}}/")
20
+ print(fact)
21
+ print(f"memory now holds {len(existing['records'])} asset(s) under '{{AGENT_NAME}}/'")
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,17 @@
1
+ # {{AGENT_NAME}}
2
+
3
+ A TypeScript Kohala agent scaffolded by `kohala init --language ts`.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ npm install
9
+ npm run typecheck
10
+ kohala validate {{AGENT_NAME}}
11
+ kohala run {{AGENT_NAME}} --local
12
+ kohala deploy {{AGENT_NAME}} --dry-run
13
+ ```
14
+
15
+ Run the Kohala commands from the directory containing the `{{AGENT_NAME}}/`
16
+ folder. Edit `skills/main.ts`; use `skills/_tools.ts` for typed, governed
17
+ platform tool calls. The same loopback RPC interface is available when hosted.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "{{AGENT_NAME}}",
3
+ "charter": "Collect one interesting fact per shift and store it in memory. Keep the output short and factual.",
4
+ "toolAllowlist": ["s3.put", "s3.get", "s3.list", "notify.send", "metrics.record"],
5
+ "runtimeMode": "wrap",
6
+ "skills": {
7
+ "main": "main.ts"
8
+ },
9
+ "dependencies": [],
10
+ "caps": {
11
+ "perRunTokens": 20000,
12
+ "perDayTokens": 100000
13
+ },
14
+ "validators": [
15
+ { "type": "shape", "minBytes": 10 },
16
+ { "type": "freshness", "asset": "{{AGENT_NAME}}/latest", "maxAgeHours": 24 }
17
+ ]
18
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "{{AGENT_NAME}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "devDependencies": {
9
+ "@types/node": "^22.0.0",
10
+ "typescript": "^5.9.0"
11
+ }
12
+ }
@@ -0,0 +1,121 @@
1
+ export class ToolError extends Error {
2
+ constructor(
3
+ readonly code: string,
4
+ message: string,
5
+ ) {
6
+ super(`${code}: ${message}`);
7
+ this.name = "ToolError";
8
+ }
9
+ }
10
+
11
+ interface RpcSuccess<T> {
12
+ ok: true;
13
+ result: T;
14
+ }
15
+
16
+ interface RpcFailure {
17
+ ok: false;
18
+ error?: { code?: string; message?: string };
19
+ }
20
+
21
+ export type ToolPayload = Record<string, unknown>;
22
+
23
+ export interface ToolEnvelope<T = unknown> {
24
+ ok: boolean;
25
+ output?: T;
26
+ error?: string;
27
+ summary?: string;
28
+ }
29
+
30
+ export async function callToolRaw<T = unknown>(
31
+ toolId: string,
32
+ payload: ToolPayload,
33
+ _timeoutMs?: number,
34
+ ): Promise<ToolEnvelope<T>> {
35
+ const rpcUrl = process.env.KOHALA_RPC_URL;
36
+ if (!rpcUrl) {
37
+ throw new ToolError(
38
+ "NO_RUNTIME",
39
+ "KOHALA_RPC_URL is not set. Run via `kohala run <agent> --local`.",
40
+ );
41
+ }
42
+ const response = await fetch(rpcUrl, {
43
+ method: "POST",
44
+ headers: { "content-type": "application/json" },
45
+ body: JSON.stringify({ tool: toolId, args: payload }),
46
+ });
47
+ const body = (await response.json()) as RpcSuccess<T> | RpcFailure;
48
+ if (!body.ok) {
49
+ return {
50
+ ok: false,
51
+ error: `${body.error?.code ?? "UNKNOWN"}: ${body.error?.message ?? "tool call failed"}`,
52
+ };
53
+ }
54
+ return { ok: true, output: body.result };
55
+ }
56
+
57
+ export async function callTool<T = unknown>(
58
+ toolId: string,
59
+ payload: ToolPayload,
60
+ timeoutMs?: number,
61
+ ): Promise<T> {
62
+ const result = await callToolRaw<T>(toolId, payload, timeoutMs);
63
+ if (!result.ok) {
64
+ throw new ToolError("TOOL_ERROR", result.error ?? `${toolId}: tool returned ok=false`);
65
+ }
66
+ return result.output as T;
67
+ }
68
+
69
+ type ToolFunction<T = unknown> = (payload: ToolPayload, timeoutMs?: number) => Promise<T>;
70
+
71
+ export interface KohalaTools {
72
+ [toolId: string]: ToolFunction;
73
+ s3Put: ToolFunction;
74
+ s3Get: ToolFunction;
75
+ s3List: ToolFunction<MemoryList>;
76
+ s3Delete: ToolFunction;
77
+ httpPostJson: ToolFunction;
78
+ llmComplete: ToolFunction<string>;
79
+ notifySend: ToolFunction;
80
+ metricsRecord: ToolFunction;
81
+ }
82
+
83
+ export const tools = new Proxy({} as KohalaTools, {
84
+ get: (_target, property) => {
85
+ const toolId = String(property).replace(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`);
86
+ return (payload: ToolPayload, timeoutMs?: number) => callTool(toolId, payload, timeoutMs);
87
+ },
88
+ });
89
+
90
+ export function configure(): never {
91
+ throw new ToolError(
92
+ "UNSUPPORTED",
93
+ "configure() is managed by the Kohala runtime and is unavailable locally.",
94
+ );
95
+ }
96
+
97
+ export const TOOL_IDS = [
98
+ "s3.put",
99
+ "s3.get",
100
+ "s3.list",
101
+ "s3.delete",
102
+ "http.post_json",
103
+ "llm.complete",
104
+ "notify.send",
105
+ "metrics.record",
106
+ ] as const;
107
+
108
+ export function camel(id: string): string {
109
+ return id.replace(/[._-]+(.)/g, (_match, letter: string) => letter.toUpperCase());
110
+ }
111
+
112
+ export interface MemoryRecord {
113
+ id: string;
114
+ key: string;
115
+ [key: string]: unknown;
116
+ }
117
+
118
+ export interface MemoryList {
119
+ records: MemoryRecord[];
120
+ [key: string]: unknown;
121
+ }
@@ -0,0 +1,18 @@
1
+ import { tools } from "./_tools.js";
2
+
3
+ export async function run(): Promise<void> {
4
+ const fact = `Shift ran at ${new Date().toISOString()}.`;
5
+ await tools.s3Put({ key: "{{AGENT_NAME}}/latest", body: fact });
6
+ await tools.notifySend({ channel: "dev", message: "shift completed" });
7
+ await tools.metricsRecord({ name: "facts_stored", value: 1 });
8
+ const existing = await tools.s3List({ prefix: "{{AGENT_NAME}}/" });
9
+
10
+ console.log(fact);
11
+ console.log(`memory now holds ${existing.records.length} asset(s) under "{{AGENT_NAME}}/"`);
12
+ }
13
+
14
+ // The hosted Node lane imports and invokes `run`. The local emulator executes
15
+ // this file directly and supplies KOHALA_RPC_URL.
16
+ if (process.env.KOHALA_RPC_URL) {
17
+ await run();
18
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "types": ["node"]
9
+ },
10
+ "include": ["skills/**/*.ts"]
11
+ }
@@ -1,40 +0,0 @@
1
- # {{AGENT_NAME}}
2
-
3
- A Kohala agent scaffolded by `kohala init`. Everything here runs on your own
4
- machine — no account, no billing, no waitlist.
5
-
6
- ## Files
7
-
8
- - `kohala.json` — the agent manifest: charter, tools, caps, validators.
9
- - `skills/main.py` — the skill script. Its stdout is the run output.
10
- - `skills/_tools.py` — the local tool SDK (stdlib-only). Don't edit; it is the
11
- same interface the hosted platform provides.
12
-
13
- ## Commands
14
-
15
- ```bash
16
- kohala validate {{AGENT_NAME}} # check kohala.json
17
- kohala run {{AGENT_NAME}} --local # run one shift with the local emulator
18
- kohala trace {{AGENT_NAME}} # inspect the audit trail
19
- ```
20
-
21
- Run these from the directory *containing* the `{{AGENT_NAME}}/` folder.
22
-
23
- ## What the emulator enforces (exactly like the platform)
24
-
25
- 1. **Admission** — refuses the shift if today's runs already hit `caps.perDayTokens`.
26
- 2. **Tool allowlist** — every tool call is checked; anything not in
27
- `toolAllowlist` fails with `TOOL_DENIED`.
28
- 3. **Per-run cap** — LLM calls abort with `PER_RUN_TOKEN_CAP` before crossing
29
- `caps.perRunTokens`.
30
- 4. **Validators** — `shape`, `freshness`, and `invariant` checks run on the
31
- output, with up to 2 repair attempts.
32
-
33
- Tokens are counted and shown in the trace, but nothing is ever billed locally.
34
-
35
- ## Going live (optional)
36
-
37
- ```bash
38
- kohala login # paste your pk_ key from kohala.ai
39
- kohala deploy {{AGENT_NAME}} # idempotent; never deletes anything remotely
40
- ```
@@ -1,125 +0,0 @@
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
- }