exempclaw 0.4.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/LICENSE +21 -0
- package/README.md +306 -0
- package/dist/agent/agent.d.ts +91 -0
- package/dist/agent/agent.js +258 -0
- package/dist/agent/agent.js.map +1 -0
- package/dist/agent/config.d.ts +49 -0
- package/dist/agent/config.js +58 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/persona.d.ts +39 -0
- package/dist/agent/persona.js +81 -0
- package/dist/agent/persona.js.map +1 -0
- package/dist/agents/registry.d.ts +21 -0
- package/dist/agents/registry.js +51 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/cli/approve.d.ts +17 -0
- package/dist/cli/approve.js +50 -0
- package/dist/cli/approve.js.map +1 -0
- package/dist/cli/chat.d.ts +16 -0
- package/dist/cli/chat.js +148 -0
- package/dist/cli/chat.js.map +1 -0
- package/dist/cli/demo.d.ts +7 -0
- package/dist/cli/demo.js +82 -0
- package/dist/cli/demo.js.map +1 -0
- package/dist/cli/init.d.ts +17 -0
- package/dist/cli/init.js +89 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/live.d.ts +10 -0
- package/dist/cli/live.js +109 -0
- package/dist/cli/live.js.map +1 -0
- package/dist/cli/offline.d.ts +23 -0
- package/dist/cli/offline.js +236 -0
- package/dist/cli/offline.js.map +1 -0
- package/dist/cli/probe.d.ts +23 -0
- package/dist/cli/probe.js +140 -0
- package/dist/cli/probe.js.map +1 -0
- package/dist/cli/render.d.ts +15 -0
- package/dist/cli/render.js +50 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/tui.d.ts +101 -0
- package/dist/cli/tui.js +334 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/config/index.d.ts +33 -0
- package/dist/config/index.js +48 -0
- package/dist/config/index.js.map +1 -0
- package/dist/connectors/connector.d.ts +58 -0
- package/dist/connectors/connector.js +30 -0
- package/dist/connectors/connector.js.map +1 -0
- package/dist/connectors/email/email-connector.d.ts +43 -0
- package/dist/connectors/email/email-connector.js +364 -0
- package/dist/connectors/email/email-connector.js.map +1 -0
- package/dist/connectors/github/github-connector.d.ts +52 -0
- package/dist/connectors/github/github-connector.js +271 -0
- package/dist/connectors/github/github-connector.js.map +1 -0
- package/dist/connectors/http.d.ts +34 -0
- package/dist/connectors/http.js +78 -0
- package/dist/connectors/http.js.map +1 -0
- package/dist/connectors/index.d.ts +34 -0
- package/dist/connectors/index.js +86 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/notion/notion-connector.d.ts +45 -0
- package/dist/connectors/notion/notion-connector.js +222 -0
- package/dist/connectors/notion/notion-connector.js.map +1 -0
- package/dist/connectors/slack/slack-connector.d.ts +43 -0
- package/dist/connectors/slack/slack-connector.js +291 -0
- package/dist/connectors/slack/slack-connector.js.map +1 -0
- package/dist/core/errors.d.ts +36 -0
- package/dist/core/errors.js +40 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/logger.d.ts +14 -0
- package/dist/core/logger.js +44 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/run-log.d.ts +37 -0
- package/dist/core/run-log.js +37 -0
- package/dist/core/run-log.js.map +1 -0
- package/dist/core/usage.d.ts +22 -0
- package/dist/core/usage.js +58 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/dashboard/data.d.ts +62 -0
- package/dist/dashboard/data.js +84 -0
- package/dist/dashboard/data.js.map +1 -0
- package/dist/dashboard/page.d.ts +9 -0
- package/dist/dashboard/page.js +421 -0
- package/dist/dashboard/page.js.map +1 -0
- package/dist/dashboard/server.d.ts +19 -0
- package/dist/dashboard/server.js +44 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/demo/bootstrap.d.ts +25 -0
- package/dist/demo/bootstrap.js +60 -0
- package/dist/demo/bootstrap.js.map +1 -0
- package/dist/demo/claude.d.ts +31 -0
- package/dist/demo/claude.js +230 -0
- package/dist/demo/claude.js.map +1 -0
- package/dist/demo/demo-connector.d.ts +19 -0
- package/dist/demo/demo-connector.js +168 -0
- package/dist/demo/demo-connector.js.map +1 -0
- package/dist/demo/world.d.ts +60 -0
- package/dist/demo/world.js +117 -0
- package/dist/demo/world.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +396 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/ingest.d.ts +63 -0
- package/dist/ingest/ingest.js +258 -0
- package/dist/ingest/ingest.js.map +1 -0
- package/dist/llm/claude.d.ts +97 -0
- package/dist/llm/claude.js +163 -0
- package/dist/llm/claude.js.map +1 -0
- package/dist/memory/compaction.d.ts +22 -0
- package/dist/memory/compaction.js +79 -0
- package/dist/memory/compaction.js.map +1 -0
- package/dist/memory/file-store.d.ts +28 -0
- package/dist/memory/file-store.js +110 -0
- package/dist/memory/file-store.js.map +1 -0
- package/dist/memory/store.d.ts +32 -0
- package/dist/memory/store.js +2 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +63 -0
- package/dist/orchestrator/orchestrator.js +181 -0
- package/dist/orchestrator/orchestrator.js.map +1 -0
- package/dist/orchestrator/scheduler.d.ts +33 -0
- package/dist/orchestrator/scheduler.js +67 -0
- package/dist/orchestrator/scheduler.js.map +1 -0
- package/dist/orchestrator/seen-events.d.ts +21 -0
- package/dist/orchestrator/seen-events.js +71 -0
- package/dist/orchestrator/seen-events.js.map +1 -0
- package/dist/plugins/apply.d.ts +9 -0
- package/dist/plugins/apply.js +17 -0
- package/dist/plugins/apply.js.map +1 -0
- package/dist/plugins/define.d.ts +29 -0
- package/dist/plugins/define.js +30 -0
- package/dist/plugins/define.js.map +1 -0
- package/dist/plugins/loader.d.ts +31 -0
- package/dist/plugins/loader.js +61 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/scaffold.d.ts +5 -0
- package/dist/plugins/scaffold.js +72 -0
- package/dist/plugins/scaffold.js.map +1 -0
- package/dist/tools/builtin.d.ts +8 -0
- package/dist/tools/builtin.js +63 -0
- package/dist/tools/builtin.js.map +1 -0
- package/dist/tools/tool.d.ts +84 -0
- package/dist/tools/tool.js +70 -0
- package/dist/tools/tool.js.map +1 -0
- package/dist/ui/agent-view.test.d.ts +1 -0
- package/dist/ui/agent-view.test.js +54 -0
- package/dist/ui/agent-view.test.js.map +1 -0
- package/dist/ui/agents-data.d.ts +7 -0
- package/dist/ui/agents-data.js +25 -0
- package/dist/ui/agents-data.js.map +1 -0
- package/dist/ui/app.d.ts +24 -0
- package/dist/ui/app.js +59 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/app.test.d.ts +1 -0
- package/dist/ui/app.test.js +47 -0
- package/dist/ui/app.test.js.map +1 -0
- package/dist/ui/components/key-hints.d.ts +4 -0
- package/dist/ui/components/key-hints.js +6 -0
- package/dist/ui/components/key-hints.js.map +1 -0
- package/dist/ui/components/menu.d.ts +11 -0
- package/dist/ui/components/menu.js +20 -0
- package/dist/ui/components/menu.js.map +1 -0
- package/dist/ui/create-wizard.test.d.ts +1 -0
- package/dist/ui/create-wizard.test.js +58 -0
- package/dist/ui/create-wizard.test.js.map +1 -0
- package/dist/ui/doctor-data.d.ts +6 -0
- package/dist/ui/doctor-data.js +29 -0
- package/dist/ui/doctor-data.js.map +1 -0
- package/dist/ui/history-data.d.ts +2 -0
- package/dist/ui/history-data.js +18 -0
- package/dist/ui/history-data.js.map +1 -0
- package/dist/ui/screens/agent.d.ts +8 -0
- package/dist/ui/screens/agent.js +95 -0
- package/dist/ui/screens/agent.js.map +1 -0
- package/dist/ui/screens/agents.d.ts +7 -0
- package/dist/ui/screens/agents.js +47 -0
- package/dist/ui/screens/agents.js.map +1 -0
- package/dist/ui/screens/create.d.ts +7 -0
- package/dist/ui/screens/create.js +141 -0
- package/dist/ui/screens/create.js.map +1 -0
- package/dist/ui/screens/doctor.d.ts +5 -0
- package/dist/ui/screens/doctor.js +13 -0
- package/dist/ui/screens/doctor.js.map +1 -0
- package/dist/ui/screens/history.d.ts +7 -0
- package/dist/ui/screens/history.js +50 -0
- package/dist/ui/screens/history.js.map +1 -0
- package/dist/ui/screens/home.d.ts +8 -0
- package/dist/ui/screens/home.js +35 -0
- package/dist/ui/screens/home.js.map +1 -0
- package/dist/ui/screens/plugins.d.ts +7 -0
- package/dist/ui/screens/plugins.js +40 -0
- package/dist/ui/screens/plugins.js.map +1 -0
- package/dist/ui/services.d.ts +33 -0
- package/dist/ui/services.js +67 -0
- package/dist/ui/services.js.map +1 -0
- package/dist/ui/start.d.ts +1 -0
- package/dist/ui/start.js +16 -0
- package/dist/ui/start.js.map +1 -0
- package/dist/ui/theme.d.ts +6 -0
- package/dist/ui/theme.js +26 -0
- package/dist/ui/theme.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Santana
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Exempclaw
|
|
2
|
+
|
|
3
|
+
**Run helpful AI agents from your terminal — no coding required to get started.**
|
|
4
|
+
|
|
5
|
+
Exempclaw lets you create a Claude-powered assistant that can cover a role:
|
|
6
|
+
read and reply to email, watch a Slack channel, keep notes, and follow a
|
|
7
|
+
daily routine — all under a persona you define, with you approving anything it
|
|
8
|
+
sends. It runs in a friendly menu you drive with the arrow keys. Powered
|
|
9
|
+
exclusively by the **Claude API**.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
(\/) E X E M P C L A W fleet command
|
|
13
|
+
|
|
14
|
+
❯ Agents 2 configured
|
|
15
|
+
New agent guided setup
|
|
16
|
+
History all runs, all agents
|
|
17
|
+
Plugins 1 installed
|
|
18
|
+
Doctor check my setup
|
|
19
|
+
Quit
|
|
20
|
+
|
|
21
|
+
↑↓ move · enter select · q quit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 👋 New here? Start in 30 seconds
|
|
27
|
+
|
|
28
|
+
You need [Node.js 22 or newer](https://nodejs.org) (a free, one-click install).
|
|
29
|
+
Then open your terminal and run **one** of these:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Just try it — nothing to install:
|
|
33
|
+
npx exempclaw
|
|
34
|
+
|
|
35
|
+
# …or install it for keeps:
|
|
36
|
+
npm install -g exempclaw # then type: exempclaw
|
|
37
|
+
brew install Rabadakku/tap/exempclaw # (macOS, Homebrew)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That's it. Type **`exempclaw`** and you'll land in the menu above. From there:
|
|
41
|
+
|
|
42
|
+
1. **New agent** walks you through creating your first assistant — it asks
|
|
43
|
+
plain questions (its name, what it does, how it should sound) and explains
|
|
44
|
+
every choice. No files to edit.
|
|
45
|
+
2. **Doctor** checks your setup and tells you, in plain English, exactly what
|
|
46
|
+
to fix.
|
|
47
|
+
3. **Agents** lists everyone you've created. Pick one to chat with it.
|
|
48
|
+
|
|
49
|
+
**No Claude API key yet?** Run `exempclaw demo` for a full guided tour that
|
|
50
|
+
costs nothing and sends nothing — it's all pretend data. When you're ready for
|
|
51
|
+
the real thing, get a key at
|
|
52
|
+
[console.anthropic.com](https://console.anthropic.com) and paste it where
|
|
53
|
+
Doctor tells you to.
|
|
54
|
+
|
|
55
|
+
> **Why a key?** Exempclaw runs on Claude, Anthropic's AI. The key is how your
|
|
56
|
+
> usage is billed to *your* Anthropic account — Exempclaw never uses anyone
|
|
57
|
+
> else's.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## What you can do with it
|
|
62
|
+
|
|
63
|
+
- **Create an assistant in minutes** through the guided wizard — give it a
|
|
64
|
+
name, a job, and a tone of voice.
|
|
65
|
+
- **Chat with it** right in the terminal. It thinks out loud, shows you what
|
|
66
|
+
it's doing, and streams its answer as it types.
|
|
67
|
+
- **Let it watch your channels** (email, Slack, Notion, GitHub) and handle
|
|
68
|
+
what comes in — but it always asks before sending anything, unless you tell
|
|
69
|
+
it otherwise.
|
|
70
|
+
- **Stay in control.** Every action it takes is logged. You can review the
|
|
71
|
+
full history, see what it cost, and approve or deny anything that goes out.
|
|
72
|
+
- **Teach it the ropes.** Point it at a folder of past emails or docs and it
|
|
73
|
+
distills them into lasting memory, so it starts already knowing the context.
|
|
74
|
+
- **Add new powers with plugins** — drop a folder in, restart, done.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<!-- ============================================================= -->
|
|
79
|
+
|
|
80
|
+
## 🛠️ For developers
|
|
81
|
+
|
|
82
|
+
Everything below is the technical reference. Jump to what you need:
|
|
83
|
+
|
|
84
|
+
- [How it works](#how-it-works)
|
|
85
|
+
- [Installing from source](#installing-from-source)
|
|
86
|
+
- [The agent lifecycle](#the-agent-lifecycle)
|
|
87
|
+
- [Agent config reference](#agent-config-reference)
|
|
88
|
+
- [Command reference](#command-reference)
|
|
89
|
+
- [Writing a plugin](#writing-a-plugin)
|
|
90
|
+
- [Connectors](#connectors)
|
|
91
|
+
- [Outward-action safety](#outward-action-safety)
|
|
92
|
+
- [Memory & context](#memory--context)
|
|
93
|
+
- [Project layout & tests](#project-layout--tests)
|
|
94
|
+
- [Responsible use](#responsible-use)
|
|
95
|
+
|
|
96
|
+
### How it works
|
|
97
|
+
|
|
98
|
+
A single process owns the fleet. The menu (an [ink](https://github.com/vadimdemedes/ink)
|
|
99
|
+
TUI) and every CLI subcommand drive the same orchestrator, which runs each
|
|
100
|
+
agent's tool-use loop against the Claude API.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
exempclaw ─┬─ TUI menu ───────┐
|
|
104
|
+
├─ chat / run ──────┼─ Orchestrator ─ Agent (tool-use loop)
|
|
105
|
+
├─ start (fleet) ───┘ │ └ { Claude · Tools · Memory · Connectors }
|
|
106
|
+
└─ dashboard (read-only ledger: runs, approvals, costs, memory)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Bare `exempclaw` opens the TUI when stdin/stdout are a real terminal; in a
|
|
110
|
+
pipe or CI it prints help instead. All the classic subcommands still exist for
|
|
111
|
+
scripting — the TUI is a friendlier front door, not a replacement.
|
|
112
|
+
|
|
113
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full design.
|
|
114
|
+
|
|
115
|
+
### Installing from source
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git clone https://github.com/Rabadakku/Exempclaw.git
|
|
119
|
+
cd exempclaw
|
|
120
|
+
npm install
|
|
121
|
+
npm run dev # runs the TUI from source (tsx)
|
|
122
|
+
npm run build # compiles to dist/ (the `exempclaw` bin)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`npm run dev -- <subcommand>` runs any CLI command from source, e.g.
|
|
126
|
+
`npm run dev -- doctor`.
|
|
127
|
+
|
|
128
|
+
### The agent lifecycle
|
|
129
|
+
|
|
130
|
+
**1. Define** — one JSON file per agent. The **New agent** wizard writes these
|
|
131
|
+
for you; `exempclaw init agents/sam.json` is the scriptable equivalent. Files
|
|
132
|
+
live in your agents directory (`./agents` in a project, otherwise
|
|
133
|
+
`~/.exempclaw/agents`; override with `EXEMPCLAW_AGENTS_DIR`).
|
|
134
|
+
|
|
135
|
+
**2. Ingest** *(optional, but the point)* — `exempclaw ingest <agent.json>
|
|
136
|
+
<dir>` reads exported artifacts (mail, docs, notes — any text), distills them
|
|
137
|
+
into durable role memories with Claude (people, commitments, conventions,
|
|
138
|
+
in-flight work — never secrets), and synthesizes a role briefing. The agent
|
|
139
|
+
starts already knowing the territory.
|
|
140
|
+
|
|
141
|
+
**3. Run** — `chat` for interactive work, `run` for one-shots, `start` for the
|
|
142
|
+
always-on fleet: inbound events (a new email, a Slack mention, a GitHub issue)
|
|
143
|
+
are deduplicated, routed to the owning agent, and handled in its persistent
|
|
144
|
+
context. Schedules (`every` / `dailyAt`) cover recurring duties.
|
|
145
|
+
|
|
146
|
+
**4. Observe** — every run lands in an append-only audit log: trigger, turns,
|
|
147
|
+
tokens, estimated cost, every outward action and who approved it. The TUI's
|
|
148
|
+
**History** screen browses it; `exempclaw costs` aggregates spend; `exempclaw
|
|
149
|
+
dashboard` serves the read-only **Succession Ledger** on localhost.
|
|
150
|
+
|
|
151
|
+
### Agent config reference
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"id": "jordan-support-lead",
|
|
156
|
+
"persona": {
|
|
157
|
+
"name": "Jordan",
|
|
158
|
+
"role": "Customer Support Lead",
|
|
159
|
+
"succeeds": "Alex Rivera",
|
|
160
|
+
"tone": "warm, concise, proactive",
|
|
161
|
+
"disclosure": "transparent"
|
|
162
|
+
},
|
|
163
|
+
"model": "claude-opus-4-8",
|
|
164
|
+
"effort": "high",
|
|
165
|
+
"connectors": ["email", "slack"],
|
|
166
|
+
"toolPolicies": { "slack_post_message": "ask", "email_send": "ask" },
|
|
167
|
+
"schedules": [{ "dailyAt": "09:00", "input": "Morning triage: …" }]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`disclosure` is required and is one of `transparent` (always says it's an AI),
|
|
172
|
+
`on_request` (answers truthfully when asked), or `opaque` (doesn't volunteer
|
|
173
|
+
it — but still never denies being an AI when sincerely asked, and never claims
|
|
174
|
+
to be a specific named human). See [Responsible use](#responsible-use).
|
|
175
|
+
|
|
176
|
+
### Command reference
|
|
177
|
+
|
|
178
|
+
The TUI covers the common path; these are the full subcommands (also useful
|
|
179
|
+
for scripting and automation):
|
|
180
|
+
|
|
181
|
+
| Command | What it does |
|
|
182
|
+
|---|---|
|
|
183
|
+
| *(none)* | Open the full-screen fleet menu (same as `ui`) |
|
|
184
|
+
| `ui` | Open the full-screen fleet menu explicitly |
|
|
185
|
+
| `init <path>` | Scaffold an agent config (interactive or via flags) |
|
|
186
|
+
| `chat <agent>` | Animated REPL — streamed text, live tool rows, slash commands, Ctrl-C interrupts the run |
|
|
187
|
+
| `run <agent> <input>` | One-shot run; `--json` for the full result, `--policy` to override |
|
|
188
|
+
| `start <agents…>` | Fleet mode: connector listeners + schedules until Ctrl-C |
|
|
189
|
+
| `ingest <agent> <dir>` | Distill an export directory (incl. `.eml`/`.mbox`) into role memory + briefing |
|
|
190
|
+
| `plugin create <name>` | Scaffold a new plugin with a working example tool |
|
|
191
|
+
| `plugin list` | List discovered plugins and any load errors |
|
|
192
|
+
| `dashboard [agents…]` | Read-only web ledger on 127.0.0.1 (`--port`, default 4177) |
|
|
193
|
+
| `demo` | Scripted replay of the animated TUI — no API key needed |
|
|
194
|
+
| `memory <agent>` | List/search durable memory; `--add`, `--rm` |
|
|
195
|
+
| `history <agent>` | Show the transcript; `--clear` to reset (memories kept) |
|
|
196
|
+
| `costs` | Tokens + estimated spend per agent from the audit log |
|
|
197
|
+
| `connectors` | Connector credential status |
|
|
198
|
+
| `doctor` | Environment check + live connector probes (`--no-probe` to skip) |
|
|
199
|
+
|
|
200
|
+
### Writing a plugin
|
|
201
|
+
|
|
202
|
+
A plugin adds **tools** (capabilities an agent can call) and/or **connectors**
|
|
203
|
+
(full integrations) without touching the core. Plugins can't change the LLM —
|
|
204
|
+
Exempclaw is Claude-only by design.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
exempclaw plugin create weather # scaffolds ~/.exempclaw/plugins/weather
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
That generates a working, zero-install plugin you can edit immediately:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
// ~/.exempclaw/plugins/weather/index.js
|
|
214
|
+
export default function ({ z, defineTool, definePlugin }) {
|
|
215
|
+
return definePlugin({
|
|
216
|
+
name: "weather",
|
|
217
|
+
tools: [
|
|
218
|
+
defineTool({
|
|
219
|
+
name: "weather_hello",
|
|
220
|
+
description: "Example tool — replace with your own.",
|
|
221
|
+
schema: z.object({ who: z.string() }),
|
|
222
|
+
execute: async ({ who }) => ({ content: `Hello, ${who}!` }),
|
|
223
|
+
}),
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Restart `exempclaw` and open **Plugins** — yours appears with its tools, or
|
|
230
|
+
with a load error to fix (a broken plugin never crashes the app). Plugins are
|
|
231
|
+
discovered in `~/.exempclaw/plugins` (override with `EXEMPCLAW_PLUGINS_DIR`).
|
|
232
|
+
Each scaffold ships a `PLUGIN.md` documenting the full interface, including how
|
|
233
|
+
to write connectors and how TypeScript authors can import types from
|
|
234
|
+
`exempclaw/plugin`.
|
|
235
|
+
|
|
236
|
+
### Connectors
|
|
237
|
+
|
|
238
|
+
Built-in connectors: **email** (IMAP/SMTP), **Slack** (Web API + Socket Mode),
|
|
239
|
+
**Notion**, **GitHub**. Each contributes tools and, optionally, inbound events.
|
|
240
|
+
Credentials come from the environment — `exempclaw connectors` shows status,
|
|
241
|
+
[docs/CONNECTORS.md](docs/CONNECTORS.md) explains provisioning. Adding one
|
|
242
|
+
means implementing the [`Connector`](src/connectors/connector.ts) interface;
|
|
243
|
+
the runtime and orchestrator don't change. For third-party integrations,
|
|
244
|
+
prefer a [plugin](#writing-a-plugin).
|
|
245
|
+
|
|
246
|
+
### Outward-action safety
|
|
247
|
+
|
|
248
|
+
Anything that affects the outside world (sending email, posting to Slack,
|
|
249
|
+
writing to Notion/GitHub) is marked `outward` and routes through an approval
|
|
250
|
+
policy before it executes:
|
|
251
|
+
|
|
252
|
+
- `ask` (default) — prompt to approve once, deny, or auto-approve that tool
|
|
253
|
+
for the session. In the TUI this is an inline yes/no dialog.
|
|
254
|
+
- `auto` — execute without prompting (once you trust an agent)
|
|
255
|
+
- `deny` — block all outward actions (dry-run / shadow mode)
|
|
256
|
+
|
|
257
|
+
Set the default with `EXEMPCLAW_ACTION_POLICY`, override per invocation with
|
|
258
|
+
`--policy`, and per tool in the agent config. Every decision is recorded in the
|
|
259
|
+
run log.
|
|
260
|
+
|
|
261
|
+
### Memory & context
|
|
262
|
+
|
|
263
|
+
- **Durable memory** — atomic facts with source + tags, persisted per agent
|
|
264
|
+
(`remember`/`recall` tools, the ingest pass, or `exempclaw memory --add`).
|
|
265
|
+
The most recent slice is injected into the system prompt each run.
|
|
266
|
+
- **Conversation history** — survives restarts. When the prompt outgrows
|
|
267
|
+
`EXEMPCLAW_CONTEXT_BUDGET_TOKENS` (default 200k), older turns are summarized
|
|
268
|
+
by Claude and replaced with a compact digest; recent turns stay verbatim and
|
|
269
|
+
tool-call pairs are never split.
|
|
270
|
+
- **Prompt caching** — the persona/tools prefix and the running conversation
|
|
271
|
+
carry cache breakpoints, so long-lived agents mostly pay cache-read prices.
|
|
272
|
+
|
|
273
|
+
### Project layout & tests
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
src/ui/ ink TUI (menu, screens, components)
|
|
277
|
+
src/agent/ the agent tool-use loop, persona, config
|
|
278
|
+
src/orchestrator/ fleet supervision, routing, scheduling
|
|
279
|
+
src/connectors/ email · slack · notion · github
|
|
280
|
+
src/plugins/ plugin loader + public definePlugin API
|
|
281
|
+
src/tools/ built-in tools
|
|
282
|
+
src/memory/ durable memory + history compaction
|
|
283
|
+
src/core/ logger, run log, usage/cost
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
npm test # 160+ unit tests, no network or credentials needed
|
|
288
|
+
npm run typecheck
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Responsible use
|
|
292
|
+
|
|
293
|
+
Operating an agent through a real person's role and accounts implicates
|
|
294
|
+
consent, impersonation, and bot-disclosure rules that vary by jurisdiction.
|
|
295
|
+
The framework makes the choices explicit — `disclosure` is a required persona
|
|
296
|
+
field, and even `opaque` agents are instructed never to claim to be a specific
|
|
297
|
+
named human and never to deny being an AI when sincerely asked. Approval gating
|
|
298
|
+
and the audit log exist so a human stays accountable for outward actions.
|
|
299
|
+
Having authorization for each connected account, and meeting applicable
|
|
300
|
+
notice/disclosure requirements, is on the operator.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
[MIT](LICENSE) © 2026 Joseph Santana
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ClaudeLike, EffortLevel } from "../llm/claude.js";
|
|
2
|
+
import type { Logger } from "../core/logger.js";
|
|
3
|
+
import { type UsageTotals } from "../core/usage.js";
|
|
4
|
+
import type { RunLog, TriggerKind } from "../core/run-log.js";
|
|
5
|
+
import type { MemoryStore } from "../memory/store.js";
|
|
6
|
+
import { type AgentActivity, type ApprovalRequest, type ToolRegistry } from "../tools/tool.js";
|
|
7
|
+
import type { ActionPolicy } from "../config/index.js";
|
|
8
|
+
import { type Persona } from "./persona.js";
|
|
9
|
+
export interface AgentOptions {
|
|
10
|
+
id: string;
|
|
11
|
+
persona: Persona;
|
|
12
|
+
model: string;
|
|
13
|
+
effort?: EffortLevel;
|
|
14
|
+
/** Hard cap on tool-use iterations per run, to bound runaway loops. */
|
|
15
|
+
maxIterations?: number;
|
|
16
|
+
/** Per-tool approval-policy overrides; "*" matches any tool. */
|
|
17
|
+
toolPolicies?: Record<string, ActionPolicy>;
|
|
18
|
+
/** Compact history once the prompt grows past this many tokens. */
|
|
19
|
+
contextBudgetTokens?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface AgentDeps {
|
|
22
|
+
claude: ClaudeLike;
|
|
23
|
+
tools: ToolRegistry;
|
|
24
|
+
memory: MemoryStore;
|
|
25
|
+
log: Logger;
|
|
26
|
+
actionPolicy: ActionPolicy;
|
|
27
|
+
/** Interactive approver invoked when policy is "ask" and a tool is outward. */
|
|
28
|
+
approve: (req: ApprovalRequest) => Promise<boolean>;
|
|
29
|
+
/** Audit trail; runs are recorded when provided. */
|
|
30
|
+
runLog?: RunLog;
|
|
31
|
+
}
|
|
32
|
+
/** Live-progress callbacks for terminal UX. All optional. */
|
|
33
|
+
export interface RunHooks {
|
|
34
|
+
/** A model turn is starting (1-based). Show a thinking indicator. */
|
|
35
|
+
onTurnStart?: (iteration: number) => void;
|
|
36
|
+
onText?: (delta: string) => void;
|
|
37
|
+
onToolStart?: (name: string, input: unknown) => void;
|
|
38
|
+
onToolEnd?: (name: string, ok: boolean, detail?: string) => void;
|
|
39
|
+
/** The agent called display_status — play the matching animation. */
|
|
40
|
+
onStatus?: (activity: AgentActivity, message: string) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface RunOptions {
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
hooks?: RunHooks;
|
|
45
|
+
trigger?: {
|
|
46
|
+
kind: TriggerKind;
|
|
47
|
+
detail?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface RunResult {
|
|
51
|
+
runId: string;
|
|
52
|
+
/** The final assistant text after the loop settled. */
|
|
53
|
+
text: string;
|
|
54
|
+
iterations: number;
|
|
55
|
+
stopReason: string | null;
|
|
56
|
+
usage: UsageTotals;
|
|
57
|
+
costUsd: number | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A single agent instance. Owns the Claude tool-use loop: send the conversation,
|
|
61
|
+
* execute any tool calls (gating outward ones through the approval policy), feed
|
|
62
|
+
* results back, and repeat until the model stops calling tools. History is
|
|
63
|
+
* persisted to the MemoryStore after every run so agents survive restarts, and
|
|
64
|
+
* every run is appended to the audit log.
|
|
65
|
+
*/
|
|
66
|
+
export declare class Agent {
|
|
67
|
+
private readonly opts;
|
|
68
|
+
private readonly deps;
|
|
69
|
+
private readonly log;
|
|
70
|
+
private readonly maxIterations;
|
|
71
|
+
private readonly contextBudgetTokens;
|
|
72
|
+
constructor(opts: AgentOptions, deps: AgentDeps);
|
|
73
|
+
get id(): string;
|
|
74
|
+
get persona(): Persona;
|
|
75
|
+
/**
|
|
76
|
+
* Feeds one user input (a message, an incoming email, an event) into the
|
|
77
|
+
* agent and runs the loop to completion.
|
|
78
|
+
*/
|
|
79
|
+
run(userInput: string, options?: RunOptions): Promise<RunResult>;
|
|
80
|
+
private composeSystemBlocks;
|
|
81
|
+
private tryCompact;
|
|
82
|
+
private extractText;
|
|
83
|
+
/**
|
|
84
|
+
* Executes the turn's tool calls. Read-only tools run concurrently; if any
|
|
85
|
+
* call is outward the whole batch runs sequentially so approval prompts
|
|
86
|
+
* arrive one at a time. Result order always matches call order.
|
|
87
|
+
*/
|
|
88
|
+
private executeToolCalls;
|
|
89
|
+
private runSingleTool;
|
|
90
|
+
private toolError;
|
|
91
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ToolExecutionError } from "../core/errors.js";
|
|
3
|
+
import { addUsage, contextTokens, emptyUsage, estimateCostUsd } from "../core/usage.js";
|
|
4
|
+
import { compactHistory } from "../memory/compaction.js";
|
|
5
|
+
import { evaluatePolicy, resolvePolicy, } from "../tools/tool.js";
|
|
6
|
+
import { buildSystemBlocks } from "./persona.js";
|
|
7
|
+
/**
|
|
8
|
+
* A single agent instance. Owns the Claude tool-use loop: send the conversation,
|
|
9
|
+
* execute any tool calls (gating outward ones through the approval policy), feed
|
|
10
|
+
* results back, and repeat until the model stops calling tools. History is
|
|
11
|
+
* persisted to the MemoryStore after every run so agents survive restarts, and
|
|
12
|
+
* every run is appended to the audit log.
|
|
13
|
+
*/
|
|
14
|
+
export class Agent {
|
|
15
|
+
opts;
|
|
16
|
+
deps;
|
|
17
|
+
log;
|
|
18
|
+
maxIterations;
|
|
19
|
+
contextBudgetTokens;
|
|
20
|
+
constructor(opts, deps) {
|
|
21
|
+
this.opts = opts;
|
|
22
|
+
this.deps = deps;
|
|
23
|
+
this.log = deps.log.child({ scope: "agent", agentId: opts.id });
|
|
24
|
+
this.maxIterations = opts.maxIterations ?? 25;
|
|
25
|
+
this.contextBudgetTokens = opts.contextBudgetTokens ?? 200_000;
|
|
26
|
+
}
|
|
27
|
+
get id() {
|
|
28
|
+
return this.opts.id;
|
|
29
|
+
}
|
|
30
|
+
get persona() {
|
|
31
|
+
return this.opts.persona;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Feeds one user input (a message, an incoming email, an event) into the
|
|
35
|
+
* agent and runs the loop to completion.
|
|
36
|
+
*/
|
|
37
|
+
async run(userInput, options = {}) {
|
|
38
|
+
const runId = randomUUID();
|
|
39
|
+
const startedAt = new Date().toISOString();
|
|
40
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
41
|
+
const hooks = options.hooks ?? {};
|
|
42
|
+
const outwardActions = [];
|
|
43
|
+
const messages = await this.deps.memory.loadHistory();
|
|
44
|
+
messages.push({ role: "user", content: userInput });
|
|
45
|
+
const system = await this.composeSystemBlocks();
|
|
46
|
+
const tools = this.deps.tools.toAnthropicTools();
|
|
47
|
+
let iterations = 0;
|
|
48
|
+
let stopReason = null;
|
|
49
|
+
let finalText = "";
|
|
50
|
+
let usage = emptyUsage();
|
|
51
|
+
let lastContextTokens = 0;
|
|
52
|
+
let runError;
|
|
53
|
+
try {
|
|
54
|
+
while (iterations < this.maxIterations) {
|
|
55
|
+
if (signal.aborted) {
|
|
56
|
+
stopReason = stopReason ?? "interrupted";
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
iterations++;
|
|
60
|
+
hooks.onTurnStart?.(iterations);
|
|
61
|
+
let message;
|
|
62
|
+
try {
|
|
63
|
+
message = await this.deps.claude.turn({
|
|
64
|
+
model: this.opts.model,
|
|
65
|
+
system,
|
|
66
|
+
messages,
|
|
67
|
+
tools,
|
|
68
|
+
effort: this.opts.effort,
|
|
69
|
+
signal,
|
|
70
|
+
onText: hooks.onText,
|
|
71
|
+
cacheConversation: true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (signal.aborted) {
|
|
76
|
+
// Operator interrupt — settle gracefully, keep what we have.
|
|
77
|
+
stopReason = "interrupted";
|
|
78
|
+
iterations--;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
stopReason = message.stop_reason;
|
|
84
|
+
usage = addUsage(usage, message.usage);
|
|
85
|
+
lastContextTokens = contextTokens(message.usage);
|
|
86
|
+
messages.push({ role: "assistant", content: message.content });
|
|
87
|
+
finalText = this.extractText(message.content) || finalText;
|
|
88
|
+
// Server-side pause: re-send the conversation so the API resumes.
|
|
89
|
+
if (message.stop_reason === "pause_turn")
|
|
90
|
+
continue;
|
|
91
|
+
const toolUses = message.content.filter((b) => b.type === "tool_use");
|
|
92
|
+
if (message.stop_reason !== "tool_use" || toolUses.length === 0) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const results = await this.executeToolCalls(toolUses, signal, hooks, outwardActions);
|
|
96
|
+
messages.push({ role: "user", content: results });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
runError = err.message;
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
let toSave = messages;
|
|
105
|
+
if (lastContextTokens > this.contextBudgetTokens) {
|
|
106
|
+
toSave = await this.tryCompact(messages, signal);
|
|
107
|
+
}
|
|
108
|
+
await this.deps.memory.saveHistory(toSave);
|
|
109
|
+
const costUsd = estimateCostUsd(this.opts.model, usage);
|
|
110
|
+
await this.deps.runLog
|
|
111
|
+
?.append({
|
|
112
|
+
runId,
|
|
113
|
+
agentId: this.opts.id,
|
|
114
|
+
trigger: options.trigger ?? { kind: "cli" },
|
|
115
|
+
startedAt,
|
|
116
|
+
finishedAt: new Date().toISOString(),
|
|
117
|
+
model: this.opts.model,
|
|
118
|
+
iterations,
|
|
119
|
+
stopReason,
|
|
120
|
+
usage,
|
|
121
|
+
costUsd,
|
|
122
|
+
outwardActions,
|
|
123
|
+
...(runError ? { error: runError } : {}),
|
|
124
|
+
})
|
|
125
|
+
.catch((err) => this.log.warn("failed to write run record", { error: err.message }));
|
|
126
|
+
if (!runError) {
|
|
127
|
+
this.log.info("run complete", {
|
|
128
|
+
iterations,
|
|
129
|
+
stopReason: stopReason ?? "none",
|
|
130
|
+
tokens: usage.inputTokens + usage.outputTokens,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
runId,
|
|
136
|
+
text: finalText,
|
|
137
|
+
iterations,
|
|
138
|
+
stopReason,
|
|
139
|
+
usage,
|
|
140
|
+
costUsd: estimateCostUsd(this.opts.model, usage),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async composeSystemBlocks() {
|
|
144
|
+
const memories = await this.deps.memory.allMemories();
|
|
145
|
+
const roleContext = memories
|
|
146
|
+
.slice(-50) // most recent durable knowledge; bounded to keep the prompt manageable
|
|
147
|
+
.map((m) => `- (${m.source}) ${m.text}`)
|
|
148
|
+
.join("\n");
|
|
149
|
+
return buildSystemBlocks(this.opts.persona, roleContext);
|
|
150
|
+
}
|
|
151
|
+
async tryCompact(messages, signal) {
|
|
152
|
+
try {
|
|
153
|
+
const compacted = await compactHistory(messages, {
|
|
154
|
+
summarize: (transcript) => this.deps.claude.summarize(transcript, { model: this.opts.model, signal }),
|
|
155
|
+
});
|
|
156
|
+
if (compacted !== messages) {
|
|
157
|
+
this.log.info("history compacted", { from: messages.length, to: compacted.length });
|
|
158
|
+
}
|
|
159
|
+
return compacted;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
this.log.warn("history compaction failed; keeping full history", { error: err.message });
|
|
163
|
+
return messages;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
extractText(content) {
|
|
167
|
+
return content
|
|
168
|
+
.filter((b) => b.type === "text")
|
|
169
|
+
.map((b) => b.text)
|
|
170
|
+
.join("\n")
|
|
171
|
+
.trim();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Executes the turn's tool calls. Read-only tools run concurrently; if any
|
|
175
|
+
* call is outward the whole batch runs sequentially so approval prompts
|
|
176
|
+
* arrive one at a time. Result order always matches call order.
|
|
177
|
+
*/
|
|
178
|
+
async executeToolCalls(toolUses, signal, hooks, outwardActions) {
|
|
179
|
+
const ctx = {
|
|
180
|
+
agentId: this.opts.id,
|
|
181
|
+
log: this.log,
|
|
182
|
+
signal,
|
|
183
|
+
// Tools that need ad-hoc approval resolve against the global policy.
|
|
184
|
+
requestApproval: (req) => evaluatePolicy(this.deps.actionPolicy, req, this.deps.approve),
|
|
185
|
+
emit: (event) => {
|
|
186
|
+
if (event.kind === "status")
|
|
187
|
+
hooks.onStatus?.(event.activity, event.message);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
const anyOutward = toolUses.some((call) => this.deps.tools.get(call.name)?.outward);
|
|
191
|
+
if (anyOutward) {
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const call of toolUses) {
|
|
194
|
+
results.push(await this.runSingleTool(call, ctx, hooks, outwardActions));
|
|
195
|
+
}
|
|
196
|
+
return results;
|
|
197
|
+
}
|
|
198
|
+
return Promise.all(toolUses.map((call) => this.runSingleTool(call, ctx, hooks, outwardActions)));
|
|
199
|
+
}
|
|
200
|
+
async runSingleTool(call, ctx, hooks, outwardActions) {
|
|
201
|
+
const tool = this.deps.tools.get(call.name);
|
|
202
|
+
if (!tool) {
|
|
203
|
+
return this.toolError(call.id, `unknown tool: ${call.name}`);
|
|
204
|
+
}
|
|
205
|
+
const parsed = tool.schema.safeParse(call.input);
|
|
206
|
+
if (!parsed.success) {
|
|
207
|
+
return this.toolError(call.id, `invalid input: ${parsed.error.message}`);
|
|
208
|
+
}
|
|
209
|
+
hooks.onToolStart?.(tool.name, parsed.data);
|
|
210
|
+
// Gate outward actions through the approval policy before executing.
|
|
211
|
+
if (tool.outward) {
|
|
212
|
+
const policy = resolvePolicy(this.deps.actionPolicy, this.opts.toolPolicies, tool.name);
|
|
213
|
+
const request = {
|
|
214
|
+
tool: tool.name,
|
|
215
|
+
summary: `${tool.name} requested by agent ${this.opts.id}`,
|
|
216
|
+
detail: JSON.stringify(parsed.data, null, 2),
|
|
217
|
+
};
|
|
218
|
+
const approved = await evaluatePolicy(policy, request, this.deps.approve);
|
|
219
|
+
outwardActions.push({
|
|
220
|
+
tool: tool.name,
|
|
221
|
+
approved,
|
|
222
|
+
summary: summarizeInput(parsed.data),
|
|
223
|
+
at: new Date().toISOString(),
|
|
224
|
+
});
|
|
225
|
+
if (!approved) {
|
|
226
|
+
this.log.warn("outward action denied", { tool: tool.name, policy });
|
|
227
|
+
hooks.onToolEnd?.(tool.name, false, "denied by approval policy");
|
|
228
|
+
return this.toolError(call.id, "action denied by approval policy");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
this.log.debug("executing tool", { tool: tool.name });
|
|
233
|
+
const result = await tool.execute(parsed.data, ctx);
|
|
234
|
+
hooks.onToolEnd?.(tool.name, !result.isError);
|
|
235
|
+
return {
|
|
236
|
+
type: "tool_result",
|
|
237
|
+
tool_use_id: call.id,
|
|
238
|
+
content: result.content,
|
|
239
|
+
...(result.isError ? { is_error: true } : {}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
const message = err instanceof ToolExecutionError ? err.message : `unexpected error: ${err.message}`;
|
|
244
|
+
this.log.error("tool threw", { tool: tool.name, error: message });
|
|
245
|
+
hooks.onToolEnd?.(tool.name, false, message);
|
|
246
|
+
return this.toolError(call.id, message);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
toolError(toolUseId, message) {
|
|
250
|
+
return { type: "tool_result", tool_use_id: toolUseId, content: message, is_error: true };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** One-line description of a tool input for the audit record. */
|
|
254
|
+
function summarizeInput(input) {
|
|
255
|
+
const json = JSON.stringify(input);
|
|
256
|
+
return json.length > 200 ? `${json.slice(0, 200)}…` : json;
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=agent.js.map
|