@kohala/devkit 0.1.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/cli/index.js +2246 -0
- package/dist/cli/index.js.map +1 -0
- package/package.json +81 -0
- package/templates/README.md +40 -0
- package/templates/kohala.json +17 -0
- package/templates/skills/_tools.py +125 -0
- package/templates/skills/main.py +35 -0
|
@@ -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,35 @@
|
|
|
1
|
+
"""{{AGENT_NAME}} — main skill.
|
|
2
|
+
|
|
3
|
+
Whatever this script prints to stdout is the run's output: validators check
|
|
4
|
+
it, and it shows up in `kohala run` and the trace. Use stderr for debug logs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from _tools import s3_put, s3_list, notify_send, metrics_record, run_context
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
context = run_context()
|
|
15
|
+
if context["repair_attempt"] > 0:
|
|
16
|
+
# The validators failed last time; the feedback says why.
|
|
17
|
+
print(f"repair attempt {context['repair_attempt']}: {context['validator_feedback']}",
|
|
18
|
+
file=sys.stderr)
|
|
19
|
+
|
|
20
|
+
fact = f"Shift ran at {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}."
|
|
21
|
+
|
|
22
|
+
# Store the latest result under a stable logical key.
|
|
23
|
+
s3_put("{{AGENT_NAME}}/latest", fact)
|
|
24
|
+
|
|
25
|
+
# A couple of platform tools, working locally exactly as they do hosted.
|
|
26
|
+
notify_send("dev", "shift completed")
|
|
27
|
+
metrics_record("facts_stored", 1)
|
|
28
|
+
|
|
29
|
+
existing = s3_list(prefix="{{AGENT_NAME}}/")
|
|
30
|
+
print(fact)
|
|
31
|
+
print(f"memory now holds {len(existing['records'])} asset(s) under '{{AGENT_NAME}}/'")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|