@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/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -0
- package/README.md +5 -0
- package/dist/cli/index.js +309 -63
- package/dist/cli/index.js.map +1 -1
- package/docs/DEPLOY.md +26 -0
- package/docs/MANIFEST.md +45 -1
- package/docs/QUICKSTART.md +8 -2
- package/package.json +2 -1
- package/templates/python/README.md +15 -0
- package/templates/python/kohala.json +17 -0
- package/templates/python/skills/_tools.py +71 -0
- package/templates/python/skills/main.py +25 -0
- package/templates/typescript/README.md +17 -0
- package/templates/typescript/kohala.json +18 -0
- package/templates/typescript/package.json +12 -0
- package/templates/typescript/skills/_tools.ts +121 -0
- package/templates/typescript/skills/main.ts +18 -0
- package/templates/typescript/tsconfig.json +11 -0
- package/templates/README.md +0 -40
- package/templates/skills/_tools.py +0 -125
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).
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -10,12 +10,14 @@ kohala --version
|
|
|
10
10
|
kohala doctor # checks Node, Python, keys
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Requirements: Node.js ≥ 20
|
|
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.
|
|
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,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
|
+
}
|
package/templates/README.md
DELETED
|
@@ -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
|
-
}
|