openwork 0.1.1-rc.4 → 0.1.1-rc.6
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 +22 -110
- package/out/main/index.js +295 -18
- package/out/preload/index.js +13 -9
- package/out/renderer/assets/{index-BttVUwrw.js → index-BOB_WPKv.js} +200 -192
- package/out/renderer/assets/{index-DjlJs7Yy.css → index-CK8V1Wgb.css} +69 -84
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
# openwork
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/openwork)
|
|
5
|
-
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
[![npm][npm-badge]][npm-url] [![License: MIT][license-badge]][license-url]
|
|
6
4
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+

|
|
20
13
|
|
|
21
|
-
|
|
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
|
-
|
|
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+.
|
|
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
|
-
##
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -871,11 +1007,27 @@ When delegating to subagents:
|
|
|
871
1007
|
- read_file: Read file contents
|
|
872
1008
|
- edit_file: Replace exact strings in files (must read first, provide unique old_string)
|
|
873
1009
|
- write_file: Create or overwrite files
|
|
874
|
-
- ls: List directory contents
|
|
1010
|
+
- ls: List directory contents
|
|
875
1011
|
- glob: Find files by pattern (e.g., "**/*.py")
|
|
876
1012
|
- grep: Search file contents
|
|
877
1013
|
|
|
878
|
-
All file paths
|
|
1014
|
+
All file paths should use fully qualified absolute system paths (e.g., /Users/name/project/src/file.ts).
|
|
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
|
|
879
1031
|
|
|
880
1032
|
## Code References
|
|
881
1033
|
When referencing code, use format: \`file_path:line_number\`
|
|
@@ -915,11 +1067,11 @@ function getSystemPrompt(workspacePath) {
|
|
|
915
1067
|
### File System and Paths
|
|
916
1068
|
|
|
917
1069
|
**IMPORTANT - Path Handling:**
|
|
918
|
-
- All file paths use
|
|
919
|
-
-
|
|
920
|
-
- Example:
|
|
921
|
-
- To list the workspace root, use \`ls("
|
|
922
|
-
-
|
|
1070
|
+
- All file paths use fully qualified absolute system paths
|
|
1071
|
+
- The workspace root is: \`${workspacePath}\`
|
|
1072
|
+
- Example: \`${workspacePath}/src/index.ts\`, \`${workspacePath}/README.md\`
|
|
1073
|
+
- To list the workspace root, use \`ls("${workspacePath}")\`
|
|
1074
|
+
- Always use full absolute paths for all file operations
|
|
923
1075
|
`;
|
|
924
1076
|
return workingDirSection + BASE_SYSTEM_PROMPT;
|
|
925
1077
|
}
|
|
@@ -970,18 +1122,37 @@ 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
|
|
1125
|
+
const backend = new LocalSandbox({
|
|
974
1126
|
rootDir: workspacePath,
|
|
975
|
-
virtualMode:
|
|
1127
|
+
virtualMode: false,
|
|
1128
|
+
// Use absolute system paths for consistency with shell commands
|
|
1129
|
+
timeout: 12e4,
|
|
1130
|
+
// 2 minutes
|
|
1131
|
+
maxOutputBytes: 1e5
|
|
1132
|
+
// ~100KB
|
|
976
1133
|
});
|
|
977
1134
|
const systemPrompt = getSystemPrompt(workspacePath);
|
|
1135
|
+
const filesystemSystemPrompt = `You have access to a filesystem. All file paths use fully qualified absolute system paths.
|
|
1136
|
+
|
|
1137
|
+
- ls: list files in a directory (e.g., ls("${workspacePath}"))
|
|
1138
|
+
- read_file: read a file from the filesystem
|
|
1139
|
+
- write_file: write to a file in the filesystem
|
|
1140
|
+
- edit_file: edit a file in the filesystem
|
|
1141
|
+
- glob: find files matching a pattern (e.g., "**/*.py")
|
|
1142
|
+
- grep: search for text within files
|
|
1143
|
+
|
|
1144
|
+
The workspace root is: ${workspacePath}`;
|
|
978
1145
|
const agent = deepagents.createDeepAgent({
|
|
979
1146
|
model,
|
|
980
1147
|
checkpointer: checkpointer2,
|
|
981
1148
|
backend,
|
|
982
|
-
systemPrompt
|
|
1149
|
+
systemPrompt,
|
|
1150
|
+
// Custom filesystem prompt for absolute paths (requires deepagents update)
|
|
1151
|
+
filesystemSystemPrompt,
|
|
1152
|
+
// Require human approval for all shell commands
|
|
1153
|
+
interruptOn: { execute: true }
|
|
983
1154
|
});
|
|
984
|
-
console.log("[Runtime] Deep agent created with
|
|
1155
|
+
console.log("[Runtime] Deep agent created with LocalSandbox at:", workspacePath);
|
|
985
1156
|
return agent;
|
|
986
1157
|
}
|
|
987
1158
|
let db = null;
|
|
@@ -1222,19 +1393,125 @@ function registerAgentHandlers(ipcMain) {
|
|
|
1222
1393
|
}
|
|
1223
1394
|
}
|
|
1224
1395
|
);
|
|
1225
|
-
ipcMain.
|
|
1396
|
+
ipcMain.on(
|
|
1397
|
+
"agent:resume",
|
|
1398
|
+
async (event, {
|
|
1399
|
+
threadId,
|
|
1400
|
+
command
|
|
1401
|
+
}) => {
|
|
1402
|
+
const channel = `agent:stream:${threadId}`;
|
|
1403
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1404
|
+
console.log("[Agent] Received resume request:", { threadId, command });
|
|
1405
|
+
if (!window) {
|
|
1406
|
+
console.error("[Agent] No window found for resume");
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const thread = getThread(threadId);
|
|
1410
|
+
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1411
|
+
const workspacePath = metadata.workspacePath;
|
|
1412
|
+
if (!workspacePath) {
|
|
1413
|
+
window.webContents.send(channel, {
|
|
1414
|
+
type: "error",
|
|
1415
|
+
error: "Workspace path is required"
|
|
1416
|
+
});
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const existingController = activeRuns.get(threadId);
|
|
1420
|
+
if (existingController) {
|
|
1421
|
+
existingController.abort();
|
|
1422
|
+
activeRuns.delete(threadId);
|
|
1423
|
+
}
|
|
1424
|
+
const abortController = new AbortController();
|
|
1425
|
+
activeRuns.set(threadId, abortController);
|
|
1426
|
+
try {
|
|
1427
|
+
const agent = await createAgentRuntime({ workspacePath });
|
|
1428
|
+
const config = {
|
|
1429
|
+
configurable: { thread_id: threadId },
|
|
1430
|
+
signal: abortController.signal,
|
|
1431
|
+
streamMode: ["messages", "values"],
|
|
1432
|
+
recursionLimit: 1e3
|
|
1433
|
+
};
|
|
1434
|
+
const decisionType = command?.resume?.decision || "approve";
|
|
1435
|
+
const resumeValue = { decisions: [{ type: decisionType }] };
|
|
1436
|
+
const stream = await agent.stream(new langgraph.Command({ resume: resumeValue }), config);
|
|
1437
|
+
for await (const chunk of stream) {
|
|
1438
|
+
if (abortController.signal.aborted) break;
|
|
1439
|
+
const [mode, data] = chunk;
|
|
1440
|
+
window.webContents.send(channel, {
|
|
1441
|
+
type: "stream",
|
|
1442
|
+
mode,
|
|
1443
|
+
data: JSON.parse(JSON.stringify(data))
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
window.webContents.send(channel, { type: "done" });
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
console.error("[Agent] Resume error:", error);
|
|
1449
|
+
window.webContents.send(channel, {
|
|
1450
|
+
type: "error",
|
|
1451
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1452
|
+
});
|
|
1453
|
+
} finally {
|
|
1454
|
+
activeRuns.delete(threadId);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
);
|
|
1458
|
+
ipcMain.on(
|
|
1226
1459
|
"agent:interrupt",
|
|
1227
|
-
async (
|
|
1460
|
+
async (event, { threadId, decision }) => {
|
|
1461
|
+
const channel = `agent:stream:${threadId}`;
|
|
1462
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1463
|
+
if (!window) {
|
|
1464
|
+
console.error("[Agent] No window found for interrupt response");
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1228
1467
|
const thread = getThread(threadId);
|
|
1229
1468
|
const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
|
|
1230
1469
|
const workspacePath = metadata.workspacePath;
|
|
1231
1470
|
if (!workspacePath) {
|
|
1232
|
-
|
|
1471
|
+
window.webContents.send(channel, {
|
|
1472
|
+
type: "error",
|
|
1473
|
+
error: "Workspace path is required"
|
|
1474
|
+
});
|
|
1475
|
+
return;
|
|
1233
1476
|
}
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1477
|
+
const existingController = activeRuns.get(threadId);
|
|
1478
|
+
if (existingController) {
|
|
1479
|
+
existingController.abort();
|
|
1480
|
+
activeRuns.delete(threadId);
|
|
1481
|
+
}
|
|
1482
|
+
const abortController = new AbortController();
|
|
1483
|
+
activeRuns.set(threadId, abortController);
|
|
1484
|
+
try {
|
|
1485
|
+
const agent = await createAgentRuntime({ workspacePath });
|
|
1486
|
+
const config = {
|
|
1487
|
+
configurable: { thread_id: threadId },
|
|
1488
|
+
signal: abortController.signal,
|
|
1489
|
+
streamMode: ["messages", "values"],
|
|
1490
|
+
recursionLimit: 1e3
|
|
1491
|
+
};
|
|
1492
|
+
if (decision.type === "approve") {
|
|
1493
|
+
const stream = await agent.stream(null, config);
|
|
1494
|
+
for await (const chunk of stream) {
|
|
1495
|
+
if (abortController.signal.aborted) break;
|
|
1496
|
+
const [mode, data] = chunk;
|
|
1497
|
+
window.webContents.send(channel, {
|
|
1498
|
+
type: "stream",
|
|
1499
|
+
mode,
|
|
1500
|
+
data: JSON.parse(JSON.stringify(data))
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
window.webContents.send(channel, { type: "done" });
|
|
1504
|
+
} else if (decision.type === "reject") {
|
|
1505
|
+
window.webContents.send(channel, { type: "done" });
|
|
1506
|
+
}
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
console.error("[Agent] Interrupt error:", error);
|
|
1509
|
+
window.webContents.send(channel, {
|
|
1510
|
+
type: "error",
|
|
1511
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1512
|
+
});
|
|
1513
|
+
} finally {
|
|
1514
|
+
activeRuns.delete(threadId);
|
|
1238
1515
|
}
|
|
1239
1516
|
}
|
|
1240
1517
|
);
|
package/out/preload/index.js
CHANGED
|
@@ -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
|
-
|
|
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 });
|