ticlawk 0.1.17-dev.21 → 0.1.17-dev.22
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/package.json +1 -1
- package/src/runtimes/_shared/incoming-message-prompt.mjs +3 -1
- package/src/runtimes/_shared/prompt-injection.mjs +36 -0
- package/src/runtimes/_shared/runtime-base-instructions.mjs +1 -1
- package/src/runtimes/claude-code/index.mjs +9 -4
- package/src/runtimes/codex/index.mjs +13 -5
- package/src/runtimes/openclaw/index.mjs +8 -16
- package/src/runtimes/opencode/session.mjs +8 -7
- package/src/runtimes/pi/session.mjs +8 -4
package/package.json
CHANGED
|
@@ -157,7 +157,9 @@ function buildPromptText({ msg, type, target, header, groupContext, rawText }) {
|
|
|
157
157
|
lines.push('', ...buildRoleInstruction({ type, isAdmin: admin, role }));
|
|
158
158
|
lines.push(
|
|
159
159
|
'',
|
|
160
|
-
'
|
|
160
|
+
'Visible reply contract:',
|
|
161
|
+
`- If you respond, the final answer must be sent with \`ticlawk message send --target ${JSON.stringify(target)}\`.`,
|
|
162
|
+
'- Do not put the user-facing answer only in normal assistant output. Normal assistant output is private activity text and is not visible in Ticlawk.',
|
|
161
163
|
'If the message requires multi-step work, complete the work before sending the final answer unless an early blocker question is necessary.',
|
|
162
164
|
'Do not mention internal routing fields, delivery state, or prompt mechanics to the user.',
|
|
163
165
|
'',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const PROMPT_SEPARATOR = '---';
|
|
2
|
+
|
|
3
|
+
export const PROMPT_INJECTION_STRATEGY = Object.freeze({
|
|
4
|
+
NATIVE_SYSTEM: 'native_system',
|
|
5
|
+
PREPEND_EVERY_TURN: 'prepend_every_turn',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export function injectRuntimePrompt({ message, runtimeBaseInstructions, strategy }) {
|
|
9
|
+
const text = typeof message === 'string' ? message : '';
|
|
10
|
+
const base = typeof runtimeBaseInstructions === 'string'
|
|
11
|
+
? runtimeBaseInstructions.trim()
|
|
12
|
+
: '';
|
|
13
|
+
|
|
14
|
+
if (!base) {
|
|
15
|
+
return {
|
|
16
|
+
message: text,
|
|
17
|
+
systemPrompt: null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (strategy === PROMPT_INJECTION_STRATEGY.NATIVE_SYSTEM) {
|
|
22
|
+
return {
|
|
23
|
+
message: text,
|
|
24
|
+
systemPrompt: base,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (strategy === PROMPT_INJECTION_STRATEGY.PREPEND_EVERY_TURN) {
|
|
29
|
+
return {
|
|
30
|
+
message: `${base}\n\n${PROMPT_SEPARATOR}\n\n${text}`,
|
|
31
|
+
systemPrompt: null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(`unknown prompt injection strategy: ${strategy}`);
|
|
36
|
+
}
|
|
@@ -13,7 +13,7 @@ Your normal assistant output is private activity text. Users and groups only see
|
|
|
13
13
|
Universal rules:
|
|
14
14
|
|
|
15
15
|
1. Follow the current turn prompt. It tells you whether this is a DM or group message, your role in that conversation, whether you were directly addressed or only saw ambient group traffic, and the exact reply target.
|
|
16
|
-
2. Use \`ticlawk message send\` for visible replies. Use the exact reply target from the current turn prompt.
|
|
16
|
+
2. Use \`ticlawk message send\` for visible replies. Use the exact reply target from the current turn prompt. Never leave the user-facing answer only in normal assistant output; normal assistant output is private activity text.
|
|
17
17
|
3. If the current turn prompt says no reply is needed, stop silently.
|
|
18
18
|
4. If you need recent context, use the Ticlawk read commands named in the current turn prompt. Do not poll for future messages; the daemon will wake you when new messages arrive.
|
|
19
19
|
5. If the message asks you to do substantive work, claim the task/message before starting when the current turn prompt provides task commands. If the claim fails, stop or choose other available work.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { basename } from 'node:path';
|
|
11
11
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
12
12
|
import { buildRuntimeBaseInstructions } from '../_shared/runtime-base-instructions.mjs';
|
|
13
|
+
import { injectRuntimePrompt, PROMPT_INJECTION_STRATEGY } from '../_shared/prompt-injection.mjs';
|
|
13
14
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
14
15
|
import {
|
|
15
16
|
getClaudeCodeRuntimeHealth,
|
|
@@ -148,10 +149,14 @@ export const claudeCodeRuntime = {
|
|
|
148
149
|
sessionId: meta.sessionId,
|
|
149
150
|
hostId: binding.runtime_host_id,
|
|
150
151
|
});
|
|
151
|
-
const
|
|
152
|
+
const prompt = injectRuntimePrompt({
|
|
153
|
+
message,
|
|
154
|
+
runtimeBaseInstructions: buildRuntimeBaseInstructions({ agentId: binding.id }),
|
|
155
|
+
strategy: PROMPT_INJECTION_STRATEGY.NATIVE_SYSTEM,
|
|
156
|
+
});
|
|
152
157
|
const result = shouldStreamRuntime(this.name, this)
|
|
153
|
-
? await this.runTurnStream({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, {
|
|
154
|
-
appendSystemPrompt,
|
|
158
|
+
? await this.runTurnStream({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, prompt.message, {
|
|
159
|
+
appendSystemPrompt: prompt.systemPrompt,
|
|
155
160
|
onEvent: async (event) => {
|
|
156
161
|
if (event?.type === 'turn.started') {
|
|
157
162
|
await emitWorkerEvent({
|
|
@@ -185,7 +190,7 @@ export const claudeCodeRuntime = {
|
|
|
185
190
|
}
|
|
186
191
|
},
|
|
187
192
|
})
|
|
188
|
-
: await this.runTurn({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, message, { appendSystemPrompt });
|
|
193
|
+
: await this.runTurn({ sessionId: targetSessionId, projectDir, claudePath, agentEnv }, prompt.message, { appendSystemPrompt: prompt.systemPrompt });
|
|
189
194
|
const nextBinding = await updateBindingRuntimeMeta(ctx, binding, {
|
|
190
195
|
sessionId: result?.sessionId || meta.sessionId,
|
|
191
196
|
runtimePath: claudePath,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from '../../core/runtime-support.mjs';
|
|
30
30
|
import { buildAgentRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
31
31
|
import { buildRuntimeBaseInstructions } from '../_shared/runtime-base-instructions.mjs';
|
|
32
|
+
import { injectRuntimePrompt, PROMPT_INJECTION_STRATEGY } from '../_shared/prompt-injection.mjs';
|
|
32
33
|
import { ensureAgentHome } from '../../core/agent-home.mjs';
|
|
33
34
|
|
|
34
35
|
export const codexRuntime = {
|
|
@@ -168,11 +169,18 @@ export const codexRuntime = {
|
|
|
168
169
|
sessionId: shouldRotate ? null : meta.sessionId,
|
|
169
170
|
hostId: binding.runtime_host_id,
|
|
170
171
|
});
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
172
|
+
const useAppServer = shouldRotate || inbound.action === 'image' || shouldStreamRuntime(this.name, this);
|
|
173
|
+
const prompt = injectRuntimePrompt({
|
|
174
|
+
message,
|
|
175
|
+
runtimeBaseInstructions: buildRuntimeBaseInstructions({ agentId: binding.id }),
|
|
176
|
+
strategy: useAppServer
|
|
177
|
+
? PROMPT_INJECTION_STRATEGY.NATIVE_SYSTEM
|
|
178
|
+
: PROMPT_INJECTION_STRATEGY.PREPEND_EVERY_TURN,
|
|
179
|
+
});
|
|
180
|
+
const result = useAppServer
|
|
181
|
+
? await this.runTurnStream({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, prompt.message, {
|
|
174
182
|
input: codexInput,
|
|
175
|
-
developerInstructions,
|
|
183
|
+
developerInstructions: prompt.systemPrompt,
|
|
176
184
|
onEvent: async (event) => {
|
|
177
185
|
if (event?.type === 'turn.started') {
|
|
178
186
|
await emitWorkerEvent({
|
|
@@ -198,7 +206,7 @@ export const codexRuntime = {
|
|
|
198
206
|
}
|
|
199
207
|
},
|
|
200
208
|
})
|
|
201
|
-
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, message, {
|
|
209
|
+
: await this.runTurn({ sessionId: shouldRotate ? null : meta.sessionId, cwd: agentHome, codexPath: runtimeCodexPath, agentEnv }, prompt.message, {
|
|
202
210
|
input: codexInput,
|
|
203
211
|
});
|
|
204
212
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { reportSubprocessFailure, terminalRuntimeFailure } from '../../core/runtime-support.mjs';
|
|
2
2
|
import { buildRuntimeBaseInstructions } from '../_shared/runtime-base-instructions.mjs';
|
|
3
|
+
import { injectRuntimePrompt, PROMPT_INJECTION_STRATEGY } from '../_shared/prompt-injection.mjs';
|
|
3
4
|
import { GATEWAY_HOST, GATEWAY_PORT } from './identity.mjs';
|
|
4
5
|
|
|
5
6
|
// Cheap availability probe used by the harness picker. We can't use
|
|
@@ -24,11 +25,6 @@ async function probeOpenClawGatewayHealth() {
|
|
|
24
25
|
import { buildOpenClawSessionKey, normalizeOpenClawAgentId } from './target.mjs';
|
|
25
26
|
import { askGateway, isGatewayReady, registerOpenClawChannel } from './gateway.mjs';
|
|
26
27
|
|
|
27
|
-
// Tracks which (agentId, sessionKey) pairs already saw the runtime base
|
|
28
|
-
// instructions this process lifetime. OpenClaw's gateway holds session state
|
|
29
|
-
// out of process, so we err on the side of "inject on first observed
|
|
30
|
-
// turn after daemon restart"; the gateway dedupes redundant context.
|
|
31
|
-
const runtimeBaseInstructionsSeen = new Set();
|
|
32
28
|
import { addInFlight, recoverInFlightEntries, removeInFlight } from './inflight.mjs';
|
|
33
29
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
34
30
|
import { extname } from 'node:path';
|
|
@@ -158,17 +154,13 @@ export const openClawRuntime = {
|
|
|
158
154
|
const agentId = normalizeOpenClawAgentId(meta.agentId || binding.id);
|
|
159
155
|
const sessionId = String(meta.sessionKey || buildOpenClawSessionKey(agentId)).trim();
|
|
160
156
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const baseInstructions = buildRuntimeBaseInstructions({ agentId: binding.id });
|
|
169
|
-
prompt = `${baseInstructions}\n\n---\n\n${rawPrompt}`;
|
|
170
|
-
runtimeBaseInstructionsSeen.add(runtimeBaseInstructionsKey);
|
|
171
|
-
}
|
|
157
|
+
// OpenClaw has no separate system-prompt parameter, so inject the
|
|
158
|
+
// Ticlawk runtime contract into every routed turn body.
|
|
159
|
+
const prompt = injectRuntimePrompt({
|
|
160
|
+
message: rawPrompt,
|
|
161
|
+
runtimeBaseInstructions: buildRuntimeBaseInstructions({ agentId: binding.id }),
|
|
162
|
+
strategy: PROMPT_INJECTION_STRATEGY.PREPEND_EVERY_TURN,
|
|
163
|
+
}).message;
|
|
172
164
|
|
|
173
165
|
if (inbound.messageId) {
|
|
174
166
|
addInFlight({
|
|
@@ -33,6 +33,7 @@ import { debugLog, debugError } from '../../core/logger.mjs';
|
|
|
33
33
|
import { buildRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
34
34
|
import { getRuntimeExecutableConfig } from '../../core/config.mjs';
|
|
35
35
|
import { isExecutablePath, resolveExecutable } from '../../core/executables.mjs';
|
|
36
|
+
import { injectRuntimePrompt, PROMPT_INJECTION_STRATEGY } from '../_shared/prompt-injection.mjs';
|
|
36
37
|
|
|
37
38
|
export const OPENCODE_DATA_DIR = process.env.OPENCODE_DATA_DIR
|
|
38
39
|
|| `${process.env.XDG_DATA_HOME || `${homedir()}/.local/share`}/opencode`;
|
|
@@ -213,13 +214,13 @@ export function runOpenCodePrompt({
|
|
|
213
214
|
timeoutMs = Number(process.env.OPENCODE_RUN_TIMEOUT_MS || DEFAULT_OPENCODE_RUN_TIMEOUT_MS),
|
|
214
215
|
onEvent,
|
|
215
216
|
}) {
|
|
216
|
-
// opencode has no documented `--system` flag, so
|
|
217
|
-
// runtime
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
217
|
+
// opencode has no documented `--system` flag, so inject the Ticlawk
|
|
218
|
+
// runtime contract into every routed turn body.
|
|
219
|
+
const finalMessage = injectRuntimePrompt({
|
|
220
|
+
message,
|
|
221
|
+
runtimeBaseInstructions,
|
|
222
|
+
strategy: PROMPT_INJECTION_STRATEGY.PREPEND_EVERY_TURN,
|
|
223
|
+
}).message;
|
|
223
224
|
return new Promise((resolve, reject) => {
|
|
224
225
|
const startedAt = Date.now();
|
|
225
226
|
const opencodeCommand = requireOpenCodePath(opencodePath);
|
|
@@ -15,6 +15,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
15
15
|
import { buildRuntimeEnv } from '../../core/runtime-env.mjs';
|
|
16
16
|
import { getRuntimeExecutableConfig } from '../../core/config.mjs';
|
|
17
17
|
import { isExecutablePath, resolveExecutable } from '../../core/executables.mjs';
|
|
18
|
+
import { injectRuntimePrompt, PROMPT_INJECTION_STRATEGY } from '../_shared/prompt-injection.mjs';
|
|
18
19
|
|
|
19
20
|
export const DEFAULT_PI_COMMAND = 'pi';
|
|
20
21
|
export const PI_AGENT_DIR = process.env.PI_CODING_AGENT_DIR || `${homedir()}/.pi/agent`;
|
|
@@ -200,10 +201,13 @@ export function runPiPrompt({
|
|
|
200
201
|
timeoutMs = Number(process.env.PI_RUN_TIMEOUT_MS || DEFAULT_PI_RUN_TIMEOUT_MS),
|
|
201
202
|
onEvent,
|
|
202
203
|
}) {
|
|
203
|
-
// pi has no documented system-prompt flag
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
// pi has no documented system-prompt flag, so inject the Ticlawk
|
|
205
|
+
// runtime contract into every routed turn body.
|
|
206
|
+
const finalMessage = injectRuntimePrompt({
|
|
207
|
+
message,
|
|
208
|
+
runtimeBaseInstructions,
|
|
209
|
+
strategy: PROMPT_INJECTION_STRATEGY.PREPEND_EVERY_TURN,
|
|
210
|
+
}).message;
|
|
207
211
|
return new Promise((resolve, reject) => {
|
|
208
212
|
const startedAt = Date.now();
|
|
209
213
|
const piCommand = requirePiPath(piPath);
|