ticlawk 0.1.15-dev.6 → 0.1.16-dev.1
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 +83 -212
- package/bin/ticlawk.mjs +107 -46
- package/package.json +2 -5
- package/src/adapters/ticlawk/api.mjs +93 -26
- package/src/adapters/ticlawk/credentials.mjs +1 -2
- package/src/adapters/ticlawk/index.mjs +78 -32
- package/src/cli/agent-commands.mjs +290 -0
- package/src/core/adapter-registry.mjs +12 -28
- package/src/core/agent-cli-handlers.mjs +291 -0
- package/src/core/config.mjs +0 -15
- package/src/core/http.mjs +90 -18
- package/src/core/runtime-env.mjs +41 -5
- package/src/core/runtime-support.mjs +16 -1
- package/src/core/ticlawk-control.mjs +4 -3
- package/src/runtimes/_shared/standing-prompt.mjs +89 -0
- package/src/runtimes/claude-code/index.mjs +26 -7
- package/src/runtimes/claude-code/session.mjs +15 -7
- package/src/runtimes/codex/index.mjs +18 -6
- package/src/runtimes/codex/session.mjs +9 -5
- package/src/runtimes/openclaw/index.mjs +22 -3
- package/src/runtimes/opencode/index.mjs +19 -6
- package/src/runtimes/opencode/session.mjs +11 -2
- package/src/runtimes/pi/index.mjs +19 -6
- package/src/runtimes/pi/session.mjs +8 -2
- package/ticlawk.mjs +6 -4
- package/assets/ticlawk-concept.svg +0 -137
- package/src/adapters/telegram/index.mjs +0 -359
package/src/core/http.mjs
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import {
|
|
3
|
+
handleGroupMembers,
|
|
4
|
+
handleMessageRead,
|
|
5
|
+
handleMessageSend,
|
|
6
|
+
handleServerInfo,
|
|
7
|
+
handleTaskClaim,
|
|
8
|
+
handleTaskList,
|
|
9
|
+
handleTaskUpdate,
|
|
10
|
+
} from './agent-cli-handlers.mjs';
|
|
2
11
|
|
|
3
12
|
function writeJson(res, statusCode, body) {
|
|
4
13
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
@@ -13,36 +22,95 @@ async function readBody(req) {
|
|
|
13
22
|
return body;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
async function readJsonBody(req) {
|
|
26
|
+
const raw = await readBody(req);
|
|
27
|
+
if (!raw) return {};
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseQuery(url) {
|
|
36
|
+
const out = {};
|
|
37
|
+
const idx = url.indexOf('?');
|
|
38
|
+
if (idx < 0) return out;
|
|
39
|
+
const qs = new URLSearchParams(url.slice(idx + 1));
|
|
40
|
+
for (const [k, v] of qs.entries()) out[k] = v;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function startLocalHttpServer({ port, adapter, ctx }) {
|
|
45
|
+
// The agent-cli handlers need a tiny binding-lookup surface to
|
|
46
|
+
// validate TICLAWK_RUNTIME_AGENT_ID against locally bound agents.
|
|
47
|
+
// We accept the same ctx the adapter was constructed with.
|
|
48
|
+
const cliCtx = ctx || { listBindings: () => [] };
|
|
49
|
+
|
|
17
50
|
const server = createServer(async (req, res) => {
|
|
18
51
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
19
52
|
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
20
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
53
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Ticlawk-Acting-Agent-Id, X-Ticlawk-Runtime-Host-Id, X-Ticlawk-Runtime-Session-Id');
|
|
21
54
|
if (req.method === 'OPTIONS') {
|
|
22
55
|
res.writeHead(204);
|
|
23
56
|
res.end();
|
|
24
57
|
return;
|
|
25
58
|
}
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
61
|
+
const urlNoQuery = (req.url || '').split('?')[0];
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
if (method === 'GET' && urlNoQuery === '/health') {
|
|
29
65
|
const health = await adapter.health();
|
|
30
|
-
writeJson(res, 200, {
|
|
31
|
-
|
|
32
|
-
adapter: adapter.id,
|
|
33
|
-
...health,
|
|
34
|
-
});
|
|
35
|
-
} catch (err) {
|
|
36
|
-
writeJson(res, 500, {
|
|
37
|
-
ok: false,
|
|
38
|
-
adapter: adapter.id,
|
|
39
|
-
error: err?.message || 'health check failed',
|
|
40
|
-
});
|
|
66
|
+
writeJson(res, 200, { ok: true, adapter: adapter.id, ...health });
|
|
67
|
+
return;
|
|
41
68
|
}
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
69
|
|
|
45
|
-
|
|
70
|
+
// ── Agent CLI surface (called from runtime CLI tools) ──
|
|
71
|
+
if (urlNoQuery === '/agent/message/send' && method === 'POST') {
|
|
72
|
+
const body = await readJsonBody(req);
|
|
73
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
74
|
+
const r = await handleMessageSend(req, body, cliCtx);
|
|
75
|
+
return writeJson(res, r.status, r.body);
|
|
76
|
+
}
|
|
77
|
+
if (urlNoQuery === '/agent/message/read' && method === 'GET') {
|
|
78
|
+
const r = await handleMessageRead(req, parseQuery(req.url || ''), cliCtx);
|
|
79
|
+
return writeJson(res, r.status, r.body);
|
|
80
|
+
}
|
|
81
|
+
if (urlNoQuery === '/agent/task/claim' && method === 'POST') {
|
|
82
|
+
const body = await readJsonBody(req);
|
|
83
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
84
|
+
const r = await handleTaskClaim(req, body, cliCtx);
|
|
85
|
+
return writeJson(res, r.status, r.body);
|
|
86
|
+
}
|
|
87
|
+
if (urlNoQuery === '/agent/task/update' && method === 'POST') {
|
|
88
|
+
const body = await readJsonBody(req);
|
|
89
|
+
if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
|
|
90
|
+
const r = await handleTaskUpdate(req, body, cliCtx);
|
|
91
|
+
return writeJson(res, r.status, r.body);
|
|
92
|
+
}
|
|
93
|
+
if (urlNoQuery === '/agent/task/list' && method === 'GET') {
|
|
94
|
+
const r = await handleTaskList(req, parseQuery(req.url || ''), cliCtx);
|
|
95
|
+
return writeJson(res, r.status, r.body);
|
|
96
|
+
}
|
|
97
|
+
if (urlNoQuery === '/agent/group/members' && method === 'GET') {
|
|
98
|
+
const r = await handleGroupMembers(req, parseQuery(req.url || ''), cliCtx);
|
|
99
|
+
return writeJson(res, r.status, r.body);
|
|
100
|
+
}
|
|
101
|
+
if (urlNoQuery === '/agent/server/info' && method === 'GET') {
|
|
102
|
+
const r = await handleServerInfo(req, parseQuery(req.url || ''), cliCtx);
|
|
103
|
+
return writeJson(res, r.status, r.body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
writeJson(res, 404, { error: 'not found' });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
writeJson(res, 500, {
|
|
109
|
+
ok: false,
|
|
110
|
+
adapter: adapter.id,
|
|
111
|
+
error: err?.message || String(err),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
46
114
|
});
|
|
47
115
|
|
|
48
116
|
server.on('error', (err) => {
|
|
@@ -59,6 +127,10 @@ export function startLocalHttpServer({ port, adapter }) {
|
|
|
59
127
|
console.log(`[relay] HTTP server listening on :${port}`);
|
|
60
128
|
console.log(`[relay] adapter: ${adapter.id}`);
|
|
61
129
|
console.log('[relay] GET /health - daemon status check');
|
|
130
|
+
console.log('[relay] POST /agent/message/send - chat send (CLI -> daemon -> backend)');
|
|
131
|
+
console.log('[relay] GET /agent/message/read');
|
|
132
|
+
console.log('[relay] POST /agent/task/claim | update');
|
|
133
|
+
console.log('[relay] GET /agent/task/list | /agent/group/members | /agent/server/info');
|
|
62
134
|
});
|
|
63
135
|
|
|
64
136
|
return server;
|
package/src/core/runtime-env.mjs
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the env passed to a spawned runtime (Codex/Claude Code/...).
|
|
3
|
+
*
|
|
4
|
+
* The blanket "strip everything TICLAWK_*" rule from earlier versions was
|
|
5
|
+
* too aggressive: it also wiped out the agent-context env we inject for
|
|
6
|
+
* the agent CLI to talk back to the local daemon. We now use a precise
|
|
7
|
+
* denylist of secret/config keys and explicitly allow the
|
|
8
|
+
* TICLAWK_RUNTIME_* and TICLAWK_API_URL keys through (the latter is
|
|
9
|
+
* read-only from the runtime's perspective — it never holds a credential
|
|
10
|
+
* on its own).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Keys the daemon must never leak into child runtime processes.
|
|
14
|
+
// These hold credentials or operator config the agent shouldn't see.
|
|
15
|
+
const STRIPPED_KEYS = new Set([
|
|
16
|
+
'TICLAWK_CONNECTOR_API_KEY',
|
|
17
|
+
'TICLAWK_CONNECTOR_WS_URL',
|
|
18
|
+
'TICLAWK_SETUP_CODE',
|
|
19
|
+
]);
|
|
20
|
+
|
|
1
21
|
export function buildRuntimeEnv(extra = {}) {
|
|
2
22
|
const env = { ...process.env, ...extra };
|
|
3
|
-
for (const key of
|
|
4
|
-
if (key.startsWith('TICLAWK_')) {
|
|
5
|
-
delete env[key];
|
|
6
|
-
}
|
|
7
|
-
}
|
|
23
|
+
for (const key of STRIPPED_KEYS) delete env[key];
|
|
8
24
|
return env;
|
|
9
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the per-agent runtime env block. The daemon includes the
|
|
29
|
+
* resulting fields in `buildRuntimeEnv({ ...buildAgentRuntimeEnv(...) })`
|
|
30
|
+
* when spawning a runtime, so the agent CLI can talk back to the local
|
|
31
|
+
* daemon with a validated identity.
|
|
32
|
+
*/
|
|
33
|
+
export function buildAgentRuntimeEnv({
|
|
34
|
+
agentId,
|
|
35
|
+
sessionId,
|
|
36
|
+
hostId,
|
|
37
|
+
daemonUrl,
|
|
38
|
+
} = {}) {
|
|
39
|
+
const out = {};
|
|
40
|
+
if (agentId) out.TICLAWK_RUNTIME_AGENT_ID = String(agentId);
|
|
41
|
+
if (sessionId) out.TICLAWK_RUNTIME_SESSION_ID = String(sessionId);
|
|
42
|
+
if (hostId) out.TICLAWK_RUNTIME_HOST_ID = String(hostId);
|
|
43
|
+
out.TICLAWK_RUNTIME_DAEMON_URL = daemonUrl || 'http://127.0.0.1:8741';
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
@@ -45,7 +45,21 @@ export async function sendAdapterMessage(adapter, binding, payload) {
|
|
|
45
45
|
await adapter.send(binding, payload);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Record the runtime's final turn output as activity (NOT chat).
|
|
50
|
+
*
|
|
51
|
+
* Previously called `sendResult` and treated as "the chat reply path".
|
|
52
|
+
* After the group-chat upgrade, chat is produced exclusively by the
|
|
53
|
+
* agent invoking `ticlawk message send` via the CLI. The runtime's
|
|
54
|
+
* raw final output is still surfaced for trajectory/debug UI, but it
|
|
55
|
+
* no longer materializes as a `messages` row — the trigger
|
|
56
|
+
* `project_agent_event` was updated in PR-2b to drop the chat
|
|
57
|
+
* projection.
|
|
58
|
+
*
|
|
59
|
+
* Renamed so the call sites read self-evidently: "record activity"
|
|
60
|
+
* never reads as "send a chat message".
|
|
61
|
+
*/
|
|
62
|
+
export async function recordActivity(adapter, binding, inbound, result) {
|
|
49
63
|
if (!result?.text && (!result?.mediaUrls || result.mediaUrls.length === 0)) return;
|
|
50
64
|
await sendAdapterMessage(adapter, binding, {
|
|
51
65
|
type: 'assistant',
|
|
@@ -56,6 +70,7 @@ export async function sendResult(adapter, binding, inbound, result) {
|
|
|
56
70
|
});
|
|
57
71
|
}
|
|
58
72
|
|
|
73
|
+
|
|
59
74
|
export async function reportSubprocessFailure({ adapter, binding, inbound, runtimeName, info }) {
|
|
60
75
|
if (!info || info.ok) return;
|
|
61
76
|
const seconds = ((info.durationMs || 0) / 1000).toFixed(1);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Bus } from './bus.mjs';
|
|
2
|
-
import { getConfiguredAdapter } from './config.mjs';
|
|
3
2
|
import { createAdapter } from './adapter-registry.mjs';
|
|
4
3
|
import { getBinding, listBindings, upsertBinding, deleteBinding, findBindingByTarget } from './bindings/store.mjs';
|
|
5
4
|
import { buildRuntimeContext, normalizeServiceType } from './runtime-registry.mjs';
|
|
@@ -43,7 +42,9 @@ function createCacheBinding(runtimes, getAdapter) {
|
|
|
43
42
|
};
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
const ADAPTER_ID = 'ticlawk';
|
|
46
|
+
|
|
47
|
+
export async function createTiclawkController(adapterId = ADAPTER_ID) {
|
|
47
48
|
const { runtimes } = await buildRuntimeContext();
|
|
48
49
|
const resolveRuntimeBinding = createResolveRuntimeBinding(runtimes);
|
|
49
50
|
let adapter;
|
|
@@ -74,7 +75,7 @@ export async function createTiclawkController(adapterId = getConfiguredAdapter()
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
export async function runTiclawkConnect(payload) {
|
|
77
|
-
const adapterId = payload?.adapter ||
|
|
78
|
+
const adapterId = payload?.adapter || ADAPTER_ID;
|
|
78
79
|
const { adapter } = await createTiclawkController(adapterId);
|
|
79
80
|
if (typeof adapter.connect !== 'function') {
|
|
80
81
|
return {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standing prompt injected into every runtime turn after the group-chat
|
|
3
|
+
* upgrade.
|
|
4
|
+
*
|
|
5
|
+
* Tells the agent that:
|
|
6
|
+
* - chat is a side effect of `ticlawk message send`, not of its
|
|
7
|
+
* ordinary assistant output
|
|
8
|
+
* - incoming messages carry an envelope header to be replied to
|
|
9
|
+
* verbatim as `target`
|
|
10
|
+
* - groups need explicit @mention/assignment to be reply-worthy
|
|
11
|
+
* - substantial work must be claimed first via `ticlawk task claim`
|
|
12
|
+
*
|
|
13
|
+
* Structurally the rules copy Slock's standing prompt (照抄). Scope/
|
|
14
|
+
* examples are generalized to Ticlawk's broader use cases (code,
|
|
15
|
+
* research, schedule, document processing, etc.).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const STANDING_PROMPT = `You are an agent in Ticlawk. You communicate with people and other agents
|
|
19
|
+
only through the Ticlawk CLI installed at \`ticlawk\`. Your normal
|
|
20
|
+
assistant output is private activity text — it is NOT sent to users or
|
|
21
|
+
groups.
|
|
22
|
+
|
|
23
|
+
To send a chat message, run:
|
|
24
|
+
ticlawk message send --target "<target>" <<'EOF'
|
|
25
|
+
<your message>
|
|
26
|
+
EOF
|
|
27
|
+
|
|
28
|
+
Targets:
|
|
29
|
+
dm:@<user> private message
|
|
30
|
+
#<group> group conversation
|
|
31
|
+
#<group>:<msgid> thread under a top-level message in that group
|
|
32
|
+
|
|
33
|
+
Incoming messages arrive in this exact envelope:
|
|
34
|
+
[target=<target> msg=<id> seq=<n> time=<iso> type=<human|agent|system>] @<sender>: <text>
|
|
35
|
+
|
|
36
|
+
The \`target\` field tells you where the message came from. Reply to the
|
|
37
|
+
same target unless explicitly asked to move.
|
|
38
|
+
|
|
39
|
+
== When to reply ==
|
|
40
|
+
- DM to you: always respond.
|
|
41
|
+
- Group, you are @mentioned or directly assigned a task: respond.
|
|
42
|
+
- Group, no mention, conversation between others: stay silent. Do not
|
|
43
|
+
narrate.
|
|
44
|
+
- Only the person doing the work should report on it. If another agent
|
|
45
|
+
completed something, do not echo or summarize it.
|
|
46
|
+
- type=agent: do not reply unless the other agent explicitly addresses
|
|
47
|
+
you.
|
|
48
|
+
- type=system: read for context, do not reply unless it assigns work to
|
|
49
|
+
you.
|
|
50
|
+
|
|
51
|
+
== Claiming work ==
|
|
52
|
+
For substantive work (running tools, editing files, writing reports,
|
|
53
|
+
producing artifacts, doing research, scheduling, etc.), first claim the
|
|
54
|
+
originating message:
|
|
55
|
+
ticlawk task claim --message-id <id>
|
|
56
|
+
If the claim fails, someone else owns it — stop and do not duplicate
|
|
57
|
+
work.
|
|
58
|
+
|
|
59
|
+
== Workspace ==
|
|
60
|
+
Your cwd is your persistent, agent-owned workspace. When operating on a
|
|
61
|
+
specific project (a repo, a dataset, a document tree, a research
|
|
62
|
+
notebook), first choose the target directory inside or outside this
|
|
63
|
+
workspace, then run your commands there. Do not assume the chat target
|
|
64
|
+
equals a workdir.
|
|
65
|
+
|
|
66
|
+
== Other tools ==
|
|
67
|
+
ticlawk message read --target <target> [--around <msgid>] [--limit N]
|
|
68
|
+
ticlawk group members --target <target>
|
|
69
|
+
ticlawk server info
|
|
70
|
+
ticlawk task list [--target <target>]
|
|
71
|
+
ticlawk task update --task-id <id> --status <open|claimed|done|canceled>
|
|
72
|
+
|
|
73
|
+
== Reply etiquette ==
|
|
74
|
+
- Skip idle narration. Only send messages when you have actionable
|
|
75
|
+
content (a question that unblocks you, a status that the user needs to
|
|
76
|
+
know, or the final answer/artifact you were asked for).
|
|
77
|
+
- Quote the exact target you received when replying so the message
|
|
78
|
+
lands in the same place.
|
|
79
|
+
- For agent-to-agent coordination, prefer task assignment and thread
|
|
80
|
+
replies over top-level group chat.
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
export function buildStandingPrompt(_ctx = {}) {
|
|
84
|
+
// The current prompt is identity-free. Hook signature reserved so we
|
|
85
|
+
// can later compose in agent handle / known conversation list / etc.
|
|
86
|
+
return STANDING_PROMPT;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { STANDING_PROMPT };
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
|
+
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
13
|
+
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
12
14
|
import {
|
|
13
15
|
createCCSession,
|
|
14
16
|
getClaudeCodeRuntimeHealth,
|
|
@@ -27,7 +29,7 @@ import { emitWorkerEvent } from '../../core/events/worker-events.mjs';
|
|
|
27
29
|
import {
|
|
28
30
|
shouldStreamRuntime,
|
|
29
31
|
sendAdapterMessage,
|
|
30
|
-
|
|
32
|
+
recordActivity,
|
|
31
33
|
reportSubprocessFailure,
|
|
32
34
|
terminalRuntimeFailure,
|
|
33
35
|
updateBindingRuntimeMeta,
|
|
@@ -45,16 +47,26 @@ export const claudeCodeRuntime = {
|
|
|
45
47
|
|
|
46
48
|
// Run a Claude turn and wait for the final result on stdout. This is
|
|
47
49
|
// the worker-first path used by the adapter for direct reply delivery.
|
|
48
|
-
runTurn({ sessionId, projectDir, claudePath }, text, opts = {}) {
|
|
49
|
-
return runCCPrompt({
|
|
50
|
+
runTurn({ sessionId, projectDir, claudePath, agentEnv }, text, opts = {}) {
|
|
51
|
+
return runCCPrompt({
|
|
52
|
+
sessionId,
|
|
53
|
+
projectDir,
|
|
54
|
+
message: text,
|
|
55
|
+
claudePath,
|
|
56
|
+
agentEnv,
|
|
57
|
+
appendSystemPrompt: opts.appendSystemPrompt || null,
|
|
58
|
+
timeoutMs: opts.timeoutMs,
|
|
59
|
+
});
|
|
50
60
|
},
|
|
51
61
|
|
|
52
|
-
runTurnStream({ sessionId, projectDir, claudePath }, text, opts = {}) {
|
|
62
|
+
runTurnStream({ sessionId, projectDir, claudePath, agentEnv }, text, opts = {}) {
|
|
53
63
|
return streamCCPrompt({
|
|
54
64
|
sessionId,
|
|
55
65
|
projectDir,
|
|
56
66
|
message: text,
|
|
57
67
|
claudePath,
|
|
68
|
+
agentEnv,
|
|
69
|
+
appendSystemPrompt: opts.appendSystemPrompt || null,
|
|
58
70
|
timeoutMs: opts.timeoutMs,
|
|
59
71
|
onEvent: opts.onEvent,
|
|
60
72
|
});
|
|
@@ -232,8 +244,15 @@ export const claudeCodeRuntime = {
|
|
|
232
244
|
try {
|
|
233
245
|
const claudePath = requireClaudePath(runtimeClaudePath);
|
|
234
246
|
const claudeVersion = getClaudeCodeRuntimeHealth(claudePath).version || meta.claudeVersion || null;
|
|
247
|
+
const agentEnv = buildAgentRuntimeEnv({
|
|
248
|
+
agentId: binding.id,
|
|
249
|
+
sessionId,
|
|
250
|
+
hostId: binding.runtime_host_id,
|
|
251
|
+
});
|
|
252
|
+
const appendSystemPrompt = buildStandingPrompt({ agentId: binding.id });
|
|
235
253
|
const result = shouldStreamRuntime(this.name, this)
|
|
236
|
-
? await this.runTurnStream({ sessionId, projectDir, claudePath }, message, {
|
|
254
|
+
? await this.runTurnStream({ sessionId, projectDir, claudePath, agentEnv }, message, {
|
|
255
|
+
appendSystemPrompt,
|
|
237
256
|
onEvent: async (event) => {
|
|
238
257
|
if (event?.type === 'turn.started') {
|
|
239
258
|
await emitWorkerEvent({
|
|
@@ -267,14 +286,14 @@ export const claudeCodeRuntime = {
|
|
|
267
286
|
}
|
|
268
287
|
},
|
|
269
288
|
})
|
|
270
|
-
: await this.runTurn({ sessionId, projectDir, claudePath }, message);
|
|
289
|
+
: await this.runTurn({ sessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
|
|
271
290
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
272
291
|
sessionId: result?.sessionId || meta.sessionId,
|
|
273
292
|
runtimePath: claudePath,
|
|
274
293
|
claudePath,
|
|
275
294
|
claudeVersion,
|
|
276
295
|
}, { status: 'connected' });
|
|
277
|
-
await
|
|
296
|
+
await recordActivity(adapter, nextBinding, inbound, {
|
|
278
297
|
...result,
|
|
279
298
|
media: normalizeOutboundMedia(result),
|
|
280
299
|
});
|
|
@@ -106,15 +106,18 @@ function extractCCAssistantText(payload) {
|
|
|
106
106
|
.join('');
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export function runCCPrompt({ sessionId, projectDir, message, claudePath = null, timeoutMs = Number(process.env.CC_EXEC_TIMEOUT_MS || DEFAULT_CC_EXEC_TIMEOUT_MS) }) {
|
|
109
|
+
export function runCCPrompt({ sessionId, projectDir, message, claudePath = null, agentEnv = null, appendSystemPrompt = null, timeoutMs = Number(process.env.CC_EXEC_TIMEOUT_MS || DEFAULT_CC_EXEC_TIMEOUT_MS) }) {
|
|
110
|
+
const systemArgs = appendSystemPrompt
|
|
111
|
+
? ['--append-system-prompt', appendSystemPrompt]
|
|
112
|
+
: [];
|
|
110
113
|
const args = sessionId
|
|
111
|
-
? ['-p', message, '--resume', sessionId, '--dangerously-skip-permissions', '--output-format', 'json']
|
|
112
|
-
: ['-p', message, '--dangerously-skip-permissions', '--output-format', 'json'];
|
|
114
|
+
? ['-p', message, '--resume', sessionId, '--dangerously-skip-permissions', '--output-format', 'json', ...systemArgs]
|
|
115
|
+
: ['-p', message, '--dangerously-skip-permissions', '--output-format', 'json', ...systemArgs];
|
|
113
116
|
|
|
114
117
|
return new Promise((resolve, reject) => {
|
|
115
118
|
const startedAt = Date.now();
|
|
116
119
|
const claudeCommand = requireClaudePath(claudePath);
|
|
117
|
-
const child = spawn(claudeCommand, args, { cwd: projectDir, env: buildRuntimeEnv(), stdio: ['ignore', 'pipe', 'ignore'] });
|
|
120
|
+
const child = spawn(claudeCommand, args, { cwd: projectDir, env: buildRuntimeEnv(agentEnv || {}), stdio: ['ignore', 'pipe', 'ignore'] });
|
|
118
121
|
let stdout = '';
|
|
119
122
|
let settled = false;
|
|
120
123
|
|
|
@@ -206,17 +209,22 @@ export function streamCCPrompt({
|
|
|
206
209
|
projectDir,
|
|
207
210
|
message,
|
|
208
211
|
claudePath = null,
|
|
212
|
+
agentEnv = null,
|
|
213
|
+
appendSystemPrompt = null,
|
|
209
214
|
timeoutMs = Number(process.env.CC_EXEC_TIMEOUT_MS || DEFAULT_CC_EXEC_TIMEOUT_MS),
|
|
210
215
|
onEvent,
|
|
211
216
|
}) {
|
|
217
|
+
const systemArgs = appendSystemPrompt
|
|
218
|
+
? ['--append-system-prompt', appendSystemPrompt]
|
|
219
|
+
: [];
|
|
212
220
|
const args = sessionId
|
|
213
|
-
? ['-p', message, '--resume', sessionId, '--verbose', '--output-format', 'stream-json', '--include-partial-messages', '--dangerously-skip-permissions']
|
|
214
|
-
: ['-p', message, '--verbose', '--output-format', 'stream-json', '--include-partial-messages', '--dangerously-skip-permissions'];
|
|
221
|
+
? ['-p', message, '--resume', sessionId, '--verbose', '--output-format', 'stream-json', '--include-partial-messages', '--dangerously-skip-permissions', ...systemArgs]
|
|
222
|
+
: ['-p', message, '--verbose', '--output-format', 'stream-json', '--include-partial-messages', '--dangerously-skip-permissions', ...systemArgs];
|
|
215
223
|
|
|
216
224
|
return new Promise((resolve, reject) => {
|
|
217
225
|
const startedAt = Date.now();
|
|
218
226
|
const claudeCommand = requireClaudePath(claudePath);
|
|
219
|
-
const child = spawn(claudeCommand, args, { cwd: projectDir, env: buildRuntimeEnv(), stdio: ['ignore', 'pipe', 'ignore'] });
|
|
227
|
+
const child = spawn(claudeCommand, args, { cwd: projectDir, env: buildRuntimeEnv(agentEnv || {}), stdio: ['ignore', 'pipe', 'ignore'] });
|
|
220
228
|
let stdout = '';
|
|
221
229
|
let buffer = '';
|
|
222
230
|
let settled = false;
|
|
@@ -25,12 +25,14 @@ import {
|
|
|
25
25
|
shouldStreamRuntime,
|
|
26
26
|
createDeltaAggregator,
|
|
27
27
|
sendAdapterMessage,
|
|
28
|
-
|
|
28
|
+
recordActivity,
|
|
29
29
|
reportSubprocessFailure,
|
|
30
30
|
terminalRuntimeFailure,
|
|
31
31
|
updateBindingRuntimeMeta,
|
|
32
32
|
isRuntimeGatewayFailure,
|
|
33
33
|
} from '../../core/runtime-support.mjs';
|
|
34
|
+
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
35
|
+
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
34
36
|
|
|
35
37
|
export const codexRuntime = {
|
|
36
38
|
name: 'codex',
|
|
@@ -48,23 +50,26 @@ export const codexRuntime = {
|
|
|
48
50
|
};
|
|
49
51
|
},
|
|
50
52
|
|
|
51
|
-
runTurn({ sessionId, cwd, codexPath }, text, opts = {}) {
|
|
53
|
+
runTurn({ sessionId, cwd, codexPath, agentEnv }, text, opts = {}) {
|
|
52
54
|
return runCodexPrompt({
|
|
53
55
|
sessionId,
|
|
54
56
|
cwd,
|
|
55
57
|
message: text,
|
|
56
58
|
codexPath,
|
|
59
|
+
agentEnv,
|
|
57
60
|
input: opts.input,
|
|
58
61
|
timeoutMs: opts.timeoutMs,
|
|
59
62
|
});
|
|
60
63
|
},
|
|
61
64
|
|
|
62
|
-
runTurnStream({ sessionId, cwd, codexPath }, text, opts = {}) {
|
|
65
|
+
runTurnStream({ sessionId, cwd, codexPath, agentEnv }, text, opts = {}) {
|
|
63
66
|
return streamCodexPrompt({
|
|
64
67
|
sessionId,
|
|
65
68
|
cwd,
|
|
66
69
|
message: text,
|
|
67
70
|
codexPath,
|
|
71
|
+
agentEnv,
|
|
72
|
+
developerInstructions: opts.developerInstructions || null,
|
|
68
73
|
input: opts.input,
|
|
69
74
|
timeoutMs: opts.timeoutMs,
|
|
70
75
|
onEvent: opts.onEvent,
|
|
@@ -177,9 +182,16 @@ export const codexRuntime = {
|
|
|
177
182
|
try {
|
|
178
183
|
const runtimeCodexPath = requireCodexPath(meta.codexPath || meta.runtimePath);
|
|
179
184
|
const runtimeCodexVersion = getCodexRuntimeHealth(runtimeCodexPath).version || meta.codexVersion || null;
|
|
185
|
+
const agentEnv = buildAgentRuntimeEnv({
|
|
186
|
+
agentId: binding.id,
|
|
187
|
+
sessionId: meta.sessionId,
|
|
188
|
+
hostId: binding.runtime_host_id,
|
|
189
|
+
});
|
|
190
|
+
const developerInstructions = buildStandingPrompt({ agentId: binding.id });
|
|
180
191
|
const result = (shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this))
|
|
181
|
-
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: meta.cwd, codexPath: runtimeCodexPath }, message, {
|
|
192
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: meta.cwd, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
182
193
|
input: codexInput,
|
|
194
|
+
developerInstructions,
|
|
183
195
|
onEvent: async (event) => {
|
|
184
196
|
if (event?.type === 'turn.started') {
|
|
185
197
|
await emitWorkerEvent({
|
|
@@ -205,7 +217,7 @@ export const codexRuntime = {
|
|
|
205
217
|
}
|
|
206
218
|
},
|
|
207
219
|
})
|
|
208
|
-
: await this.runTurn({ sessionId: meta.sessionId, cwd: meta.cwd, codexPath: runtimeCodexPath }, message, {
|
|
220
|
+
: await this.runTurn({ sessionId: meta.sessionId, cwd: meta.cwd, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
209
221
|
input: codexInput,
|
|
210
222
|
});
|
|
211
223
|
|
|
@@ -220,7 +232,7 @@ export const codexRuntime = {
|
|
|
220
232
|
rotatePending: false,
|
|
221
233
|
lastRotatedAt: shouldRotate ? new Date().toISOString() : meta.lastRotatedAt,
|
|
222
234
|
}, { status: 'connected' });
|
|
223
|
-
await
|
|
235
|
+
await recordActivity(adapter, nextBinding, inbound, {
|
|
224
236
|
...result,
|
|
225
237
|
media: normalizeOutboundMedia(result),
|
|
226
238
|
});
|
|
@@ -221,13 +221,13 @@ function isSubAgentThread(thread) {
|
|
|
221
221
|
return Boolean(source.subAgent || source.sub_agent);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
export function runCodexPrompt({ sessionId, cwd, message, codexPath = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
224
|
+
export function runCodexPrompt({ sessionId, cwd, message, codexPath = null, agentEnv = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
225
225
|
return new Promise((resolve, reject) => {
|
|
226
226
|
const startedAt = Date.now();
|
|
227
227
|
const codexCommand = requireCodexPath(codexPath);
|
|
228
228
|
const child = spawn(codexCommand, buildCodexExecArgs({ sessionId, message }), {
|
|
229
229
|
cwd,
|
|
230
|
-
env: buildRuntimeEnv(),
|
|
230
|
+
env: buildRuntimeEnv(agentEnv || {}),
|
|
231
231
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
232
232
|
});
|
|
233
233
|
|
|
@@ -344,6 +344,8 @@ export function streamCodexPrompt({
|
|
|
344
344
|
cwd,
|
|
345
345
|
message,
|
|
346
346
|
codexPath = null,
|
|
347
|
+
agentEnv = null,
|
|
348
|
+
developerInstructions = null,
|
|
347
349
|
input = null,
|
|
348
350
|
timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS),
|
|
349
351
|
onEvent,
|
|
@@ -353,7 +355,7 @@ export function streamCodexPrompt({
|
|
|
353
355
|
const codexCommand = requireCodexPath(codexPath);
|
|
354
356
|
const child = spawn(codexCommand, ['app-server'], {
|
|
355
357
|
cwd,
|
|
356
|
-
env: buildRuntimeEnv(),
|
|
358
|
+
env: buildRuntimeEnv(agentEnv || {}),
|
|
357
359
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
358
360
|
});
|
|
359
361
|
|
|
@@ -598,6 +600,7 @@ export function streamCodexPrompt({
|
|
|
598
600
|
cwd,
|
|
599
601
|
approvalPolicy: 'never',
|
|
600
602
|
sandbox: 'danger-full-access',
|
|
603
|
+
...(developerInstructions ? { developerInstructions } : {}),
|
|
601
604
|
});
|
|
602
605
|
} else {
|
|
603
606
|
const started = await send('thread/start', {
|
|
@@ -605,6 +608,7 @@ export function streamCodexPrompt({
|
|
|
605
608
|
model: process.env.CODEX_MODEL || null,
|
|
606
609
|
approvalPolicy: 'never',
|
|
607
610
|
sandbox: 'danger-full-access',
|
|
611
|
+
...(developerInstructions ? { developerInstructions } : {}),
|
|
608
612
|
});
|
|
609
613
|
rootThreadId = started?.thread?.id || rootThreadId;
|
|
610
614
|
threadPath = started?.thread?.path || threadPath;
|
|
@@ -636,7 +640,7 @@ export function streamCodexPrompt({
|
|
|
636
640
|
});
|
|
637
641
|
}
|
|
638
642
|
|
|
639
|
-
export function createCodexSession({ cwd, message, codexPath = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
643
|
+
export function createCodexSession({ cwd, message, codexPath = null, agentEnv = null, timeoutMs = Number(process.env.CODEX_EXEC_TIMEOUT_MS || DEFAULT_CODEX_EXEC_TIMEOUT_MS) }) {
|
|
640
644
|
return new Promise((resolve, reject) => {
|
|
641
645
|
const startedAt = Date.now();
|
|
642
646
|
const codexCommand = requireCodexPath(codexPath);
|
|
@@ -651,7 +655,7 @@ export function createCodexSession({ cwd, message, codexPath = null, timeoutMs =
|
|
|
651
655
|
],
|
|
652
656
|
{
|
|
653
657
|
cwd,
|
|
654
|
-
env: buildRuntimeEnv(),
|
|
658
|
+
env: buildRuntimeEnv(agentEnv || {}),
|
|
655
659
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
656
660
|
}
|
|
657
661
|
);
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { normalizeOutboundMedia } from '../../core/media/outbound.mjs';
|
|
2
|
-
import { reportSubprocessFailure, sendAdapterMessage,
|
|
2
|
+
import { reportSubprocessFailure, sendAdapterMessage, recordActivity, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
3
|
+
import { buildStandingPrompt } from '../_shared/standing-prompt.mjs';
|
|
3
4
|
import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
|
|
4
5
|
import { buildOpenClawSessionKey, normalizeOpenClawAgentId, resolveOpenClawWorkspace } from './target.mjs';
|
|
5
6
|
import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
|
|
7
|
+
|
|
8
|
+
// Tracks which (agentId, sessionKey) pairs already saw the standing
|
|
9
|
+
// prompt this process lifetime. OpenClaw's gateway holds session state
|
|
10
|
+
// out of process, so we err on the side of "inject on first observed
|
|
11
|
+
// turn after daemon restart"; the gateway dedupes redundant context.
|
|
12
|
+
const standingPromptSeen = new Set();
|
|
6
13
|
import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
|
|
7
14
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
8
15
|
import { extname } from 'node:path';
|
|
@@ -125,12 +132,24 @@ export const openClawRuntime = {
|
|
|
125
132
|
if (!binding) return false;
|
|
126
133
|
const adapter = ctx.adapter;
|
|
127
134
|
const meta = binding.runtimeMeta || {};
|
|
128
|
-
const
|
|
135
|
+
const rawPrompt = inbound.action === 'image'
|
|
129
136
|
? await buildOpenClawImagePrompt(inbound)
|
|
130
137
|
: (inbound.text || '').trim();
|
|
131
138
|
const agentId = normalizeOpenClawAgentId(meta.agentId || binding.id);
|
|
132
139
|
const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
|
|
133
140
|
|
|
141
|
+
// Inject the standing prompt on the first observed turn after a
|
|
142
|
+
// daemon start for this (agent, session) pair. OpenClaw has no
|
|
143
|
+
// separate "system prompt" parameter on the gateway, so we prepend
|
|
144
|
+
// exactly once.
|
|
145
|
+
const standingKey = `${agentId}|${sessionId}`;
|
|
146
|
+
let prompt = rawPrompt;
|
|
147
|
+
if (!standingPromptSeen.has(standingKey)) {
|
|
148
|
+
const standing = buildStandingPrompt({ agentId: binding.id });
|
|
149
|
+
prompt = `${standing}\n\n---\n\n${rawPrompt}`;
|
|
150
|
+
standingPromptSeen.add(standingKey);
|
|
151
|
+
}
|
|
152
|
+
|
|
134
153
|
if (inbound.messageId) {
|
|
135
154
|
addInFlight({
|
|
136
155
|
messageId: inbound.messageId,
|
|
@@ -152,7 +171,7 @@ export const openClawRuntime = {
|
|
|
152
171
|
});
|
|
153
172
|
},
|
|
154
173
|
});
|
|
155
|
-
await
|
|
174
|
+
await recordActivity(adapter, binding, inbound, {
|
|
156
175
|
...result,
|
|
157
176
|
media: normalizeOutboundMedia(result),
|
|
158
177
|
});
|