openwork 0.1.1-rc.4 → 0.1.1-rc.5

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/README.md CHANGED
@@ -1,29 +1,23 @@
1
1
  # openwork
2
2
 
3
- [![CI](https://github.com/langchain-ai/openwork/actions/workflows/ci.yml/badge.svg)](https://github.com/langchain-ai/openwork/actions/workflows/ci.yml)
4
- [![npm version](https://img.shields.io/npm/v/openwork.svg)](https://www.npmjs.com/package/openwork)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ [![npm][npm-badge]][npm-url] [![License: MIT][license-badge]][license-url]
6
4
 
7
- A tactical agent interface for [deepagentsjs](https://github.com/langchain-ai/deepagentsjs) - an opinionated harness for building deep agents with filesystem capabilities, planning, and subagent delegation.
5
+ [npm-badge]: https://img.shields.io/npm/v/openwork.svg
6
+ [npm-url]: https://www.npmjs.com/package/openwork
7
+ [license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg
8
+ [license-url]: https://opensource.org/licenses/MIT
8
9
 
9
- ![openwork screenshot](docs/screenshot.png)
10
-
11
- ## Features
10
+ A desktop interface for [deepagentsjs](https://github.com/langchain-ai/deepagentsjs) — an opinionated harness for building deep agents with filesystem capabilities planning, and subagent delegation.
12
11
 
13
- - **Chat Interface** - Stream conversations with your AI agent in real-time
14
- - **TODO Tracking** - Visual task list showing agent's planning progress
15
- - **Filesystem Browser** - See files the agent reads, writes, and edits
16
- - **Subagent Monitoring** - Track spawned subagents and their status
17
- - **Human-in-the-Loop** - Approve, edit, or reject sensitive tool calls
18
- - **Multi-Model Support** - Use Claude, GPT-4, Gemini, or local models
19
- - **Thread Persistence** - SQLite-backed conversation history
12
+ ![openwork screenshot](docs/screenshot.png)
20
13
 
21
- ## Installation
14
+ > [!CAUTION]
15
+ > openwork gives AI agents direct access to your filesystem and the ability to execute shell commands. Always review tool calls before approving them, and only run in workspaces you trust.
22
16
 
23
- ### npm (recommended)
17
+ ## Get Started
24
18
 
25
19
  ```bash
26
- # Run directly
20
+ # Run directly with npx
27
21
  npx openwork
28
22
 
29
23
  # Or install globally
@@ -31,7 +25,7 @@ npm install -g openwork
31
25
  openwork
32
26
  ```
33
27
 
34
- Requires Node.js 18+. Electron is installed automatically as a dependency.
28
+ Requires Node.js 18+.
35
29
 
36
30
  ### From Source
37
31
 
@@ -41,103 +35,21 @@ cd openwork
41
35
  npm install
42
36
  npm run dev
43
37
  ```
38
+ Or configure them in-app via the settings panel.
44
39
 
45
- ## Configuration
46
-
47
- ### API Keys
48
-
49
- openwork supports multiple LLM providers. Set your API keys via:
50
-
51
- 1. **Environment Variables** (recommended)
52
- ```bash
53
- export ANTHROPIC_API_KEY="sk-ant-..."
54
- export OPENAI_API_KEY="sk-..."
55
- export GOOGLE_API_KEY="..."
56
- ```
57
-
58
- 2. **In-App Settings** - Click the settings icon and enter your API keys securely.
59
-
60
- ### Supported Models
61
-
62
- | Provider | Models |
63
- |----------|--------|
64
- | Anthropic | Claude Sonnet 4, Claude 3.5 Sonnet, Claude 3.5 Haiku |
65
- | OpenAI | GPT-4o, GPT-4o Mini |
66
- | Google | Gemini 2.0 Flash |
67
-
68
- ## Architecture
69
-
70
- openwork is built with:
71
-
72
- - **Electron** - Cross-platform desktop framework
73
- - **React** - UI components with tactical/SCADA-inspired design
74
- - **deepagentsjs** - Agent harness with planning, filesystem, and subagents
75
- - **LangGraph** - State machine for agent orchestration
76
- - **SQLite** - Local persistence for threads and checkpoints
77
-
78
- ```
79
- ┌─────────────────────────────────────────────────────────────┐
80
- │ Electron Main Process │
81
- ├─────────────────────────────────────────────────────────────┤
82
- │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
83
- │ │ IPC Handlers│ │ SQLite │ │ DeepAgentsJS │ │
84
- │ │ - agent │ │ - threads │ │ - createAgent │ │
85
- │ │ - threads │ │ - runs │ │ - checkpointer │ │
86
- │ │ - models │ │ - assists │ │ - middleware │ │
87
- │ └─────────────┘ └─────────────┘ └─────────────────────┘ │
88
- └─────────────────────────────────────────────────────────────┘
89
-
90
- IPC Bridge
91
-
92
- ┌─────────────────────────────────────────────────────────────┐
93
- │ Electron Renderer Process │
94
- ├─────────────────────────────────────────────────────────────┤
95
- │ ┌──────────┐ ┌─────────────────────┐ ┌───────────────┐ │
96
- │ │ Sidebar │ │ Chat Interface │ │ Right Panel │ │
97
- │ │ - Threads│ │ - Messages │ │ - TODOs │ │
98
- │ │ - Model │ │ - Tool Renderers │ │ - Files │ │
99
- │ │ - Config │ │ - Streaming │ │ - Subagents │ │
100
- │ └──────────┘ └─────────────────────┘ └───────────────┘ │
101
- └─────────────────────────────────────────────────────────────┘
102
- ```
40
+ ## Supported Models
103
41
 
104
- ## Development
105
-
106
- ```bash
107
- # Install dependencies
108
- npm install
109
-
110
- # Start development server
111
- npm run dev
112
-
113
- # Build for production
114
- npm run build
115
- ```
116
-
117
- ## Releases
118
-
119
- To publish a new release:
120
-
121
- 1. Create a git tag: `git tag v0.2.0`
122
- 2. Push the tag: `git push origin v0.2.0`
123
- 3. GitHub Actions will:
124
- - Build the application
125
- - Publish to npm
126
- - Create a GitHub release
127
-
128
- ## Design System
129
-
130
- openwork uses a tactical/SCADA-inspired design system optimized for:
131
-
132
- - **Information density** - Dense layouts for monitoring agent activity
133
- - **Status at a glance** - Color-coded status indicators (nominal, warning, critical)
134
- - **Dark mode only** - Reduced eye strain for extended sessions
135
- - **Monospace typography** - JetBrains Mono for data and code
42
+ | Provider | Models |
43
+ | --------- | ----------------------------------------------------------------- |
44
+ | Anthropic | Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5, Claude Opus 4.1, Claude Sonnet 4 |
45
+ | OpenAI | GPT-5.2, GPT-5.1, o3, o3 Mini, o4 Mini, o1, GPT-4.1, GPT-4o |
136
46
 
137
47
  ## Contributing
138
48
 
139
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines.
49
+ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
50
+
51
+ Report bugs via [GitHub Issues](https://github.com/langchain-ai/openwork/issues).
140
52
 
141
53
  ## License
142
54
 
143
- MIT License - see [LICENSE](LICENSE) for details.
55
+ MIT see [LICENSE](LICENSE) for details.
package/out/main/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  const electron = require("electron");
3
3
  const path = require("path");
4
4
  const messages = require("@langchain/core/messages");
5
+ const langgraph = require("@langchain/langgraph");
5
6
  const deepagents = require("deepagents");
6
7
  const Store = require("electron-store");
7
8
  const fs$1 = require("fs/promises");
@@ -11,6 +12,8 @@ const anthropic = require("@langchain/anthropic");
11
12
  const openai = require("@langchain/openai");
12
13
  const initSqlJs = require("sql.js");
13
14
  const langgraphCheckpoint = require("@langchain/langgraph-checkpoint");
15
+ const node_child_process = require("node:child_process");
16
+ const node_crypto = require("node:crypto");
14
17
  const uuid = require("uuid");
15
18
  function _interopNamespaceDefault(e) {
16
19
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -817,6 +820,139 @@ class SqlJsSaver extends langgraphCheckpoint.BaseCheckpointSaver {
817
820
  }
818
821
  }
819
822
  }
823
+ class LocalSandbox extends deepagents.FilesystemBackend {
824
+ /** Unique identifier for this sandbox instance */
825
+ id;
826
+ timeout;
827
+ maxOutputBytes;
828
+ env;
829
+ workingDir;
830
+ constructor(options = {}) {
831
+ super({
832
+ rootDir: options.rootDir,
833
+ virtualMode: options.virtualMode,
834
+ maxFileSizeMb: options.maxFileSizeMb
835
+ });
836
+ this.id = `local-sandbox-${node_crypto.randomUUID().slice(0, 8)}`;
837
+ this.timeout = options.timeout ?? 12e4;
838
+ this.maxOutputBytes = options.maxOutputBytes ?? 1e5;
839
+ this.env = options.env ?? { ...process.env };
840
+ this.workingDir = options.rootDir ?? process.cwd();
841
+ }
842
+ /**
843
+ * Execute a shell command in the workspace directory.
844
+ *
845
+ * @param command - Shell command string to execute
846
+ * @returns ExecuteResponse with combined output, exit code, and truncation flag
847
+ *
848
+ * @example
849
+ * ```typescript
850
+ * const result = await sandbox.execute('echo "Hello World"');
851
+ * // result.output: "Hello World\n"
852
+ * // result.exitCode: 0
853
+ * // result.truncated: false
854
+ * ```
855
+ */
856
+ async execute(command) {
857
+ if (!command || typeof command !== "string") {
858
+ return {
859
+ output: "Error: Shell tool expects a non-empty command string.",
860
+ exitCode: 1,
861
+ truncated: false
862
+ };
863
+ }
864
+ return new Promise((resolve) => {
865
+ const outputParts = [];
866
+ let totalBytes = 0;
867
+ let truncated = false;
868
+ let resolved = false;
869
+ const isWindows = process.platform === "win32";
870
+ const shell = isWindows ? "cmd.exe" : "/bin/sh";
871
+ const shellArgs = isWindows ? ["/c", command] : ["-c", command];
872
+ const proc = node_child_process.spawn(shell, shellArgs, {
873
+ cwd: this.workingDir,
874
+ env: this.env,
875
+ stdio: ["ignore", "pipe", "pipe"]
876
+ });
877
+ const timeoutId = setTimeout(() => {
878
+ if (!resolved) {
879
+ resolved = true;
880
+ proc.kill("SIGTERM");
881
+ setTimeout(() => proc.kill("SIGKILL"), 1e3);
882
+ resolve({
883
+ output: `Error: Command timed out after ${(this.timeout / 1e3).toFixed(1)} seconds.`,
884
+ exitCode: null,
885
+ truncated: false
886
+ });
887
+ }
888
+ }, this.timeout);
889
+ proc.stdout.on("data", (data) => {
890
+ if (truncated) return;
891
+ const chunk = data.toString();
892
+ const newTotal = totalBytes + chunk.length;
893
+ if (newTotal > this.maxOutputBytes) {
894
+ const remaining = this.maxOutputBytes - totalBytes;
895
+ if (remaining > 0) {
896
+ outputParts.push(chunk.slice(0, remaining));
897
+ }
898
+ truncated = true;
899
+ totalBytes = this.maxOutputBytes;
900
+ } else {
901
+ outputParts.push(chunk);
902
+ totalBytes = newTotal;
903
+ }
904
+ });
905
+ proc.stderr.on("data", (data) => {
906
+ if (truncated) return;
907
+ const chunk = data.toString();
908
+ const prefixedLines = chunk.split("\n").filter((line) => line.length > 0).map((line) => `[stderr] ${line}`).join("\n");
909
+ if (prefixedLines.length === 0) return;
910
+ const withNewline = prefixedLines + (chunk.endsWith("\n") ? "\n" : "");
911
+ const newTotal = totalBytes + withNewline.length;
912
+ if (newTotal > this.maxOutputBytes) {
913
+ const remaining = this.maxOutputBytes - totalBytes;
914
+ if (remaining > 0) {
915
+ outputParts.push(withNewline.slice(0, remaining));
916
+ }
917
+ truncated = true;
918
+ totalBytes = this.maxOutputBytes;
919
+ } else {
920
+ outputParts.push(withNewline);
921
+ totalBytes = newTotal;
922
+ }
923
+ });
924
+ proc.on("close", (code, signal) => {
925
+ if (resolved) return;
926
+ resolved = true;
927
+ clearTimeout(timeoutId);
928
+ let output = outputParts.join("");
929
+ if (truncated) {
930
+ output += `
931
+
932
+ ... Output truncated at ${this.maxOutputBytes} bytes.`;
933
+ }
934
+ if (!output.trim()) {
935
+ output = "<no output>";
936
+ }
937
+ resolve({
938
+ output,
939
+ exitCode: signal ? null : code,
940
+ truncated
941
+ });
942
+ });
943
+ proc.on("error", (err) => {
944
+ if (resolved) return;
945
+ resolved = true;
946
+ clearTimeout(timeoutId);
947
+ resolve({
948
+ output: `Error: Failed to execute command: ${err.message}`,
949
+ exitCode: 1,
950
+ truncated: false
951
+ });
952
+ });
953
+ });
954
+ }
955
+ }
820
956
  const BASE_SYSTEM_PROMPT = `You are an AI assistant that helps users with various tasks including coding, research, and analysis.
821
957
 
822
958
  # Core Behavior
@@ -877,6 +1013,22 @@ When delegating to subagents:
877
1013
 
878
1014
  All file paths are virtual paths relative to the workspace root, starting with /.
879
1015
 
1016
+ ### Shell Tool
1017
+ - execute: Run shell commands in the workspace directory
1018
+
1019
+ The execute tool runs commands directly on the user's machine. Use it for:
1020
+ - Running scripts, tests, and builds (npm test, python script.py, make)
1021
+ - Git operations (git status, git diff, git commit)
1022
+ - Installing dependencies (npm install, pip install)
1023
+ - System commands (which, env, pwd)
1024
+
1025
+ **Important:**
1026
+ - All execute commands require user approval before running
1027
+ - Commands run in the workspace root directory
1028
+ - Avoid using shell for file reading (use read_file instead)
1029
+ - Avoid using shell for file searching (use grep/glob instead)
1030
+ - When running non-trivial commands, briefly explain what they do
1031
+
880
1032
  ## Code References
881
1033
  When referencing code, use format: \`file_path:line_number\`
882
1034
 
@@ -970,18 +1122,24 @@ async function createAgentRuntime(options) {
970
1122
  console.log("[Runtime] Model instance created:", typeof model);
971
1123
  const checkpointer2 = await getCheckpointer();
972
1124
  console.log("[Runtime] Checkpointer ready");
973
- const backend = new deepagents.FilesystemBackend({
1125
+ const backend = new LocalSandbox({
974
1126
  rootDir: workspacePath,
975
- virtualMode: true
1127
+ virtualMode: true,
1128
+ timeout: 12e4,
1129
+ // 2 minutes
1130
+ maxOutputBytes: 1e5
1131
+ // ~100KB
976
1132
  });
977
1133
  const systemPrompt = getSystemPrompt(workspacePath);
978
1134
  const agent = deepagents.createDeepAgent({
979
1135
  model,
980
1136
  checkpointer: checkpointer2,
981
1137
  backend,
982
- systemPrompt
1138
+ systemPrompt,
1139
+ // Require human approval for all shell commands
1140
+ interruptOn: { execute: true }
983
1141
  });
984
- console.log("[Runtime] Deep agent created with FilesystemBackend at:", workspacePath);
1142
+ console.log("[Runtime] Deep agent created with LocalSandbox at:", workspacePath);
985
1143
  return agent;
986
1144
  }
987
1145
  let db = null;
@@ -1222,19 +1380,125 @@ function registerAgentHandlers(ipcMain) {
1222
1380
  }
1223
1381
  }
1224
1382
  );
1225
- ipcMain.handle(
1383
+ ipcMain.on(
1384
+ "agent:resume",
1385
+ async (event, {
1386
+ threadId,
1387
+ command
1388
+ }) => {
1389
+ const channel = `agent:stream:${threadId}`;
1390
+ const window = electron.BrowserWindow.fromWebContents(event.sender);
1391
+ console.log("[Agent] Received resume request:", { threadId, command });
1392
+ if (!window) {
1393
+ console.error("[Agent] No window found for resume");
1394
+ return;
1395
+ }
1396
+ const thread = getThread(threadId);
1397
+ const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
1398
+ const workspacePath = metadata.workspacePath;
1399
+ if (!workspacePath) {
1400
+ window.webContents.send(channel, {
1401
+ type: "error",
1402
+ error: "Workspace path is required"
1403
+ });
1404
+ return;
1405
+ }
1406
+ const existingController = activeRuns.get(threadId);
1407
+ if (existingController) {
1408
+ existingController.abort();
1409
+ activeRuns.delete(threadId);
1410
+ }
1411
+ const abortController = new AbortController();
1412
+ activeRuns.set(threadId, abortController);
1413
+ try {
1414
+ const agent = await createAgentRuntime({ workspacePath });
1415
+ const config = {
1416
+ configurable: { thread_id: threadId },
1417
+ signal: abortController.signal,
1418
+ streamMode: ["messages", "values"],
1419
+ recursionLimit: 1e3
1420
+ };
1421
+ const decisionType = command?.resume?.decision || "approve";
1422
+ const resumeValue = { decisions: [{ type: decisionType }] };
1423
+ const stream = await agent.stream(new langgraph.Command({ resume: resumeValue }), config);
1424
+ for await (const chunk of stream) {
1425
+ if (abortController.signal.aborted) break;
1426
+ const [mode, data] = chunk;
1427
+ window.webContents.send(channel, {
1428
+ type: "stream",
1429
+ mode,
1430
+ data: JSON.parse(JSON.stringify(data))
1431
+ });
1432
+ }
1433
+ window.webContents.send(channel, { type: "done" });
1434
+ } catch (error) {
1435
+ console.error("[Agent] Resume error:", error);
1436
+ window.webContents.send(channel, {
1437
+ type: "error",
1438
+ error: error instanceof Error ? error.message : "Unknown error"
1439
+ });
1440
+ } finally {
1441
+ activeRuns.delete(threadId);
1442
+ }
1443
+ }
1444
+ );
1445
+ ipcMain.on(
1226
1446
  "agent:interrupt",
1227
- async (_event, { threadId, decision }) => {
1447
+ async (event, { threadId, decision }) => {
1448
+ const channel = `agent:stream:${threadId}`;
1449
+ const window = electron.BrowserWindow.fromWebContents(event.sender);
1450
+ if (!window) {
1451
+ console.error("[Agent] No window found for interrupt response");
1452
+ return;
1453
+ }
1228
1454
  const thread = getThread(threadId);
1229
1455
  const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
1230
1456
  const workspacePath = metadata.workspacePath;
1231
1457
  if (!workspacePath) {
1232
- throw new Error("Workspace path is required");
1458
+ window.webContents.send(channel, {
1459
+ type: "error",
1460
+ error: "Workspace path is required"
1461
+ });
1462
+ return;
1233
1463
  }
1234
- const agent = await createAgentRuntime({ workspacePath });
1235
- const config = { configurable: { thread_id: threadId } };
1236
- if (decision.type === "approve") {
1237
- await agent.invoke(null, config);
1464
+ const existingController = activeRuns.get(threadId);
1465
+ if (existingController) {
1466
+ existingController.abort();
1467
+ activeRuns.delete(threadId);
1468
+ }
1469
+ const abortController = new AbortController();
1470
+ activeRuns.set(threadId, abortController);
1471
+ try {
1472
+ const agent = await createAgentRuntime({ workspacePath });
1473
+ const config = {
1474
+ configurable: { thread_id: threadId },
1475
+ signal: abortController.signal,
1476
+ streamMode: ["messages", "values"],
1477
+ recursionLimit: 1e3
1478
+ };
1479
+ if (decision.type === "approve") {
1480
+ const stream = await agent.stream(null, config);
1481
+ for await (const chunk of stream) {
1482
+ if (abortController.signal.aborted) break;
1483
+ const [mode, data] = chunk;
1484
+ window.webContents.send(channel, {
1485
+ type: "stream",
1486
+ mode,
1487
+ data: JSON.parse(JSON.stringify(data))
1488
+ });
1489
+ }
1490
+ window.webContents.send(channel, { type: "done" });
1491
+ } else if (decision.type === "reject") {
1492
+ window.webContents.send(channel, { type: "done" });
1493
+ }
1494
+ } catch (error) {
1495
+ console.error("[Agent] Interrupt error:", error);
1496
+ window.webContents.send(channel, {
1497
+ type: "error",
1498
+ error: error instanceof Error ? error.message : "Unknown error"
1499
+ });
1500
+ } finally {
1501
+ activeRuns.delete(threadId);
1238
1502
  }
1239
1503
  }
1240
1504
  );
@@ -21,17 +21,14 @@ const api = {
21
21
  agent: {
22
22
  // Send message and receive events via callback
23
23
  invoke: (threadId, message, onEvent) => {
24
- console.log("[Preload] invoke() called", { threadId, message: message.substring(0, 50) });
25
24
  const channel = `agent:stream:${threadId}`;
26
25
  const handler = (_, data) => {
27
- console.log("[Preload] Received event:", data.type);
28
26
  onEvent(data);
29
27
  if (data.type === "done" || data.type === "error") {
30
28
  electron.ipcRenderer.removeListener(channel, handler);
31
29
  }
32
30
  };
33
31
  electron.ipcRenderer.on(channel, handler);
34
- console.log("[Preload] Sending agent:invoke IPC");
35
32
  electron.ipcRenderer.send("agent:invoke", { threadId, message });
36
33
  return () => {
37
34
  electron.ipcRenderer.removeListener(channel, handler);
@@ -39,10 +36,8 @@ const api = {
39
36
  },
40
37
  // Stream agent events for useStream transport
41
38
  streamAgent: (threadId, message, command, onEvent) => {
42
- console.log("[Preload] streamAgent() called", { threadId, message: message.substring(0, 50) });
43
39
  const channel = `agent:stream:${threadId}`;
44
40
  const handler = (_, data) => {
45
- console.log("[Preload] Received stream event:", data.type);
46
41
  onEvent(data);
47
42
  if (data.type === "done" || data.type === "error") {
48
43
  electron.ipcRenderer.removeListener(channel, handler);
@@ -50,18 +45,27 @@ const api = {
50
45
  };
51
46
  electron.ipcRenderer.on(channel, handler);
52
47
  if (command) {
53
- console.log("[Preload] Sending agent:resume IPC");
54
48
  electron.ipcRenderer.send("agent:resume", { threadId, command });
55
49
  } else {
56
- console.log("[Preload] Sending agent:invoke IPC");
57
50
  electron.ipcRenderer.send("agent:invoke", { threadId, message });
58
51
  }
59
52
  return () => {
60
53
  electron.ipcRenderer.removeListener(channel, handler);
61
54
  };
62
55
  },
63
- interrupt: (threadId, decision) => {
64
- return electron.ipcRenderer.invoke("agent:interrupt", { threadId, decision });
56
+ interrupt: (threadId, decision, onEvent) => {
57
+ const channel = `agent:stream:${threadId}`;
58
+ const handler = (_, data) => {
59
+ onEvent?.(data);
60
+ if (data.type === "done" || data.type === "error") {
61
+ electron.ipcRenderer.removeListener(channel, handler);
62
+ }
63
+ };
64
+ electron.ipcRenderer.on(channel, handler);
65
+ electron.ipcRenderer.send("agent:interrupt", { threadId, decision });
66
+ return () => {
67
+ electron.ipcRenderer.removeListener(channel, handler);
68
+ };
65
69
  },
66
70
  cancel: (threadId) => {
67
71
  return electron.ipcRenderer.invoke("agent:cancel", { threadId });