agent-relay-runner 0.104.5 → 0.105.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/package.json +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +114 -23
- package/plugins/claude/monitors/relay-monitor.ts +2 -2
- package/src/adapter.ts +4 -1
- package/src/adapters/codex.ts +2 -2
- package/src/profile-home.ts +2 -2
- package/src/relay-instructions.ts +14 -56
- package/src/runner-core.ts +3 -3
- package/src/template-resolver.ts +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.105.1",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"agent-relay-providers": "0.104.0",
|
|
24
|
-
"agent-relay-sdk": "0.2.
|
|
24
|
+
"agent-relay-sdk": "0.2.92",
|
|
25
25
|
"callmux": "0.23.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
@@ -847,13 +847,109 @@ function messageBodyMaxCharsFromEnv() {
|
|
|
847
847
|
return process.env.AGENT_RELAY_MESSAGE_BODY_MAX_CHARS;
|
|
848
848
|
}
|
|
849
849
|
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
850
|
+
// sdk/src/prompt-template-defaults.ts
|
|
851
|
+
function lines(...parts) {
|
|
852
|
+
return parts.join(`
|
|
853
|
+
`);
|
|
854
|
+
}
|
|
855
|
+
var RUNNER_PROMPT_TEMPLATE_DEFAULTS = [
|
|
856
|
+
{
|
|
857
|
+
slug: "relay.manual.claude",
|
|
858
|
+
category: "runner",
|
|
859
|
+
title: "Claude Relay Manual",
|
|
860
|
+
description: "Bundled CLAUDE.md relay manual written into provisioned Claude homes.",
|
|
861
|
+
template: lines("# Agent Relay", "", "- Agent Relay messages may come from humans, channels, or other agents.", "- Read the delivered message body and do the requested work.", "- Reply through Relay only when the sender still needs an answer.", "- Prefer the inline MCP tools `relay_reply` or `relay_send_message` for the actual answer to an incoming Relay message.", "- Do not send `/message` with the useful answer and then a separate `/reply` that only says it was sent.", "- If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.", '- If the useful response was already delivered through Relay, do not send an extra "sent", "done", or "drafts sent" confirmation unless the user explicitly asked for one.', "- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.", "- NEVER reply to a notification-class message. The server marks these and renders a single `\u21AA Notification \u2014 no reply needed.` line instead of the reply reminder \u2014 it is a fire-and-forget signal (a merge notice, lifecycle event, or FYI). Act on the information if relevant, but do not send any reply, status confirmation, or reaction back.", '- Use `agent-relay /react <messageId> <emoji>` instead of a text reply for lightweight acknowledgement, approval, thanks, or "good job" after a completed work update.', "- Good reaction uses: acknowledge praise with \uD83D\uDC4D or \u2764\uFE0F, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.", "- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.", "- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.", "- Use `agent-relay get-message <id>` when a delivered preview is truncated.", "- Use `agent-relay /status --json` to inspect your current Relay identity and `agent-relay /guide` for command details."),
|
|
862
|
+
defaultTemplate: lines("# Agent Relay", "", "- Agent Relay messages may come from humans, channels, or other agents.", "- Read the delivered message body and do the requested work.", "- Reply through Relay only when the sender still needs an answer.", "- Prefer the inline MCP tools `relay_reply` or `relay_send_message` for the actual answer to an incoming Relay message.", "- Do not send `/message` with the useful answer and then a separate `/reply` that only says it was sent.", "- If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.", '- If the useful response was already delivered through Relay, do not send an extra "sent", "done", or "drafts sent" confirmation unless the user explicitly asked for one.', "- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.", "- NEVER reply to a notification-class message. The server marks these and renders a single `\u21AA Notification \u2014 no reply needed.` line instead of the reply reminder \u2014 it is a fire-and-forget signal (a merge notice, lifecycle event, or FYI). Act on the information if relevant, but do not send any reply, status confirmation, or reaction back.", '- Use `agent-relay /react <messageId> <emoji>` instead of a text reply for lightweight acknowledgement, approval, thanks, or "good job" after a completed work update.', "- Good reaction uses: acknowledge praise with \uD83D\uDC4D or \u2764\uFE0F, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.", "- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.", "- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.", "- Use `agent-relay get-message <id>` when a delivered preview is truncated.", "- Use `agent-relay /status --json` to inspect your current Relay identity and `agent-relay /guide` for command details."),
|
|
863
|
+
variables: []
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
slug: "relay.context.claude",
|
|
867
|
+
category: "runner",
|
|
868
|
+
title: "Claude Relay Context",
|
|
869
|
+
description: "One-line relay context note for Claude sessions.",
|
|
870
|
+
template: "[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.",
|
|
871
|
+
defaultTemplate: "[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.",
|
|
872
|
+
variables: []
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
slug: "relay.context.claude.readonly",
|
|
876
|
+
category: "runner",
|
|
877
|
+
title: "Claude Relay Context (Read Only)",
|
|
878
|
+
description: "Read-only relay context note for Claude sessions.",
|
|
879
|
+
template: lines("[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.", "", "This Claude session is running with restricted read-only Relay permissions. Do not invoke Agent Relay skills. If you need to reply, use this Bash command shape:", "", 'agent-relay /reply <messageId> "<your reply>"'),
|
|
880
|
+
defaultTemplate: lines("[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.", "", "This Claude session is running with restricted read-only Relay permissions. Do not invoke Agent Relay skills. If you need to reply, use this Bash command shape:", "", 'agent-relay /reply <messageId> "<your reply>"'),
|
|
881
|
+
variables: []
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
slug: "relay.context.codex",
|
|
885
|
+
category: "runner",
|
|
886
|
+
title: "Codex Relay Context",
|
|
887
|
+
description: "Relay context note injected into Codex sessions.",
|
|
888
|
+
template: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id \u2014 relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own \u2014 find them later with relay_find_agents spawnedBy:me. For replies, prefer relay_reply or relay_send_message; CLI fallback: agent-relay /reply <messageId> \"<your reply>\". If a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide",
|
|
889
|
+
defaultTemplate: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id \u2014 relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own \u2014 find them later with relay_find_agents spawnedBy:me. For replies, prefer relay_reply or relay_send_message; CLI fallback: agent-relay /reply <messageId> \"<your reply>\". If a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide",
|
|
890
|
+
variables: []
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
slug: "workspace.lifecycle",
|
|
894
|
+
category: "runner",
|
|
895
|
+
title: "Runner Workspace Lifecycle Note",
|
|
896
|
+
description: "Lifecycle note injected by the runner for isolated workspaces.",
|
|
897
|
+
template: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run `agent-relay workspace ready` \u2014 Relay rebases onto the latest {{baseRef}}, lands your work, and pushes for you. If the installed `agent-relay` binary is stale and says the workspace command is unknown, run the repo-local fallback: `bun src/index.ts workspace ready`.", "After `ready`, the status becomes `review_requested` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after `ready` and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
|
|
898
|
+
defaultTemplate: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run `agent-relay workspace ready` \u2014 Relay rebases onto the latest {{baseRef}}, lands your work, and pushes for you. If the installed `agent-relay` binary is stale and says the workspace command is unknown, run the repo-local fallback: `bun src/index.ts workspace ready`.", "After `ready`, the status becomes `review_requested` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after `ready` and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
|
|
899
|
+
variables: ["branch", "baseRef"]
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
slug: "workspace.deps.symlink",
|
|
903
|
+
category: "runner",
|
|
904
|
+
title: "Runner Workspace Deps Symlink Note",
|
|
905
|
+
description: "Symlinked node_modules caveat injected by the runner.",
|
|
906
|
+
template: "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout \u2014 dependencies are already installed and ready to use. Do NOT run a clean dependency install (`bun install` / `npm install` / `pnpm install`): it writes through the symlink and mutates the main checkout's shared node_modules. Build caches written under node_modules are shared too. If typecheck/build fails on a missing module (a dependency added to the base AFTER this worktree was created), run `agent-relay workspace deps` \u2014 it re-provisions only the stale dirs with a real isolated install, safely, without touching the shared node_modules. If you genuinely need to change dependencies in isolation, ask the host to spawn with AGENT_RELAY_WORKSPACE_DEPS=install.",
|
|
907
|
+
defaultTemplate: "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout \u2014 dependencies are already installed and ready to use. Do NOT run a clean dependency install (`bun install` / `npm install` / `pnpm install`): it writes through the symlink and mutates the main checkout's shared node_modules. Build caches written under node_modules are shared too. If typecheck/build fails on a missing module (a dependency added to the base AFTER this worktree was created), run `agent-relay workspace deps` \u2014 it re-provisions only the stale dirs with a real isolated install, safely, without touching the shared node_modules. If you genuinely need to change dependencies in isolation, ask the host to spawn with AGENT_RELAY_WORKSPACE_DEPS=install.",
|
|
908
|
+
variables: []
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
slug: "workspace.deps.none",
|
|
912
|
+
category: "runner",
|
|
913
|
+
title: "Runner Workspace Deps None Note",
|
|
914
|
+
description: "Missing dependency provisioning caveat injected by the runner.",
|
|
915
|
+
template: "[agent-relay] Isolated workspace: dependencies were not provisioned (AGENT_RELAY_WORKSPACE_DEPS=none). You may need to install node_modules before typecheck/test/build work.",
|
|
916
|
+
defaultTemplate: "[agent-relay] Isolated workspace: dependencies were not provisioned (AGENT_RELAY_WORKSPACE_DEPS=none). You may need to install node_modules before typecheck/test/build work.",
|
|
917
|
+
variables: []
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
slug: "workspace.symlinks",
|
|
921
|
+
category: "runner",
|
|
922
|
+
title: "Runner Workspace Symlink Note",
|
|
923
|
+
description: "Write-through symlink caveat injected by the runner.",
|
|
924
|
+
template: "[agent-relay] Isolated workspace: these untracked paths are SYMLINKED from the main checkout: {{linked}}. They resolve to the real files in main, so editing or deleting them writes THROUGH to main \u2014 treat them as read-only unless you intend to change main.",
|
|
925
|
+
defaultTemplate: "[agent-relay] Isolated workspace: these untracked paths are SYMLINKED from the main checkout: {{linked}}. They resolve to the real files in main, so editing or deleting them writes THROUGH to main \u2014 treat them as read-only unless you intend to change main.",
|
|
926
|
+
variables: []
|
|
927
|
+
}
|
|
928
|
+
];
|
|
853
929
|
|
|
854
|
-
|
|
930
|
+
// runner/src/template-resolver.ts
|
|
931
|
+
var RUNNER_DEFAULTS = new Map(RUNNER_PROMPT_TEMPLATE_DEFAULTS.map((template) => [template.slug, template.defaultTemplate]));
|
|
932
|
+
var templateCache = new Map;
|
|
933
|
+
function renderValue(value) {
|
|
934
|
+
return value ?? "";
|
|
935
|
+
}
|
|
936
|
+
function applyVariables(template, vars) {
|
|
937
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => renderValue(vars[key]));
|
|
938
|
+
}
|
|
939
|
+
function renderRunnerTemplate(slug, vars = {}) {
|
|
940
|
+
const template = templateCache.get(slug) ?? RUNNER_DEFAULTS.get(slug);
|
|
941
|
+
if (template === undefined)
|
|
942
|
+
throw new Error(`unknown runner prompt template slug: ${slug}`);
|
|
943
|
+
return applyVariables(template, vars);
|
|
944
|
+
}
|
|
855
945
|
|
|
856
|
-
|
|
946
|
+
// runner/src/relay-instructions.ts
|
|
947
|
+
function claudeRelayContext() {
|
|
948
|
+
return renderRunnerTemplate("relay.context.claude");
|
|
949
|
+
}
|
|
950
|
+
function claudeReadOnlyRelayContext() {
|
|
951
|
+
return renderRunnerTemplate("relay.context.claude.readonly");
|
|
952
|
+
}
|
|
857
953
|
function relayReplyActionText(messageId) {
|
|
858
954
|
return `relay_reply (messageId: ${messageId}, body: "<your reply>"). CLI fallback: agent-relay /reply ${messageId} "<your reply>"`;
|
|
859
955
|
}
|
|
@@ -865,9 +961,9 @@ function workspaceDepsNote(input) {
|
|
|
865
961
|
return "";
|
|
866
962
|
switch (input.depsMode) {
|
|
867
963
|
case "symlink":
|
|
868
|
-
return "
|
|
964
|
+
return renderRunnerTemplate("workspace.deps.symlink");
|
|
869
965
|
case "none":
|
|
870
|
-
return "
|
|
966
|
+
return renderRunnerTemplate("workspace.deps.none");
|
|
871
967
|
default:
|
|
872
968
|
return "";
|
|
873
969
|
}
|
|
@@ -877,17 +973,12 @@ function workspaceLifecycleNote(input) {
|
|
|
877
973
|
return "";
|
|
878
974
|
const branch = input.branch ? `\`${input.branch}\`` : "an isolated agent branch";
|
|
879
975
|
const base = input.baseRef ? `\`${input.baseRef}\`` : "the base branch";
|
|
880
|
-
return
|
|
881
|
-
`[agent-relay] Isolated workspace: you are in a git worktree on branch ${branch}, based on ${base} \u2014 NOT the main checkout. Other agents may work in parallel and land to ${base}, so ${base} will move under you. That is expected; don't fight it.`,
|
|
882
|
-
`Do NOT push this branch yourself \u2014 not with \`git push\`, not with \`tl push\` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run \`agent-relay workspace ready\` \u2014 Relay rebases onto the latest ${base}, lands your work, and pushes for you. If the installed \`agent-relay\` binary is stale and says the workspace command is unknown, run the repo-local fallback: \`bun src/index.ts workspace ready\`.`,
|
|
883
|
-
`After \`ready\`, the status becomes \`review_requested\` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after \`ready\` and go idle; do NOT wait or poll the steward queue. Relay wakes you with \`landed-success\` when your branch lands and refreshes, or \`landed-failure\` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a \`--N\` suffix \u2014 expected, keep working there. Never \`cd\` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. \`agent-relay workspace status\` anytime shows your current state and the exact next step.`
|
|
884
|
-
].join(`
|
|
885
|
-
`);
|
|
976
|
+
return renderRunnerTemplate("workspace.lifecycle", { branch, baseRef: base });
|
|
886
977
|
}
|
|
887
978
|
function workspaceSymlinksNote(linked) {
|
|
888
979
|
if (!linked.length)
|
|
889
980
|
return "";
|
|
890
|
-
return
|
|
981
|
+
return renderRunnerTemplate("workspace.symlinks", { linked: linked.join(", ") });
|
|
891
982
|
}
|
|
892
983
|
function workspaceDepsNoteFromEnv(env = process.env) {
|
|
893
984
|
const json = env.AGENT_RELAY_WORKSPACE_JSON;
|
|
@@ -948,7 +1039,7 @@ function providerAttachmentText(message) {
|
|
|
948
1039
|
const refs = attachmentRefs(message);
|
|
949
1040
|
if (!refs.length)
|
|
950
1041
|
return;
|
|
951
|
-
const
|
|
1042
|
+
const lines2 = refs.flatMap((ref) => {
|
|
952
1043
|
const artifactId = typeof ref.artifactId === "string" ? ref.artifactId : "unknown-artifact";
|
|
953
1044
|
const source = isRecord(ref.ref) ? [
|
|
954
1045
|
typeof ref.ref.provider === "string" ? ref.ref.provider : undefined,
|
|
@@ -974,7 +1065,7 @@ function providerAttachmentText(message) {
|
|
|
974
1065
|
];
|
|
975
1066
|
return [
|
|
976
1067
|
"Attachments:",
|
|
977
|
-
...
|
|
1068
|
+
...lines2,
|
|
978
1069
|
...guidance
|
|
979
1070
|
].join(`
|
|
980
1071
|
`);
|
|
@@ -984,18 +1075,18 @@ function providerAttachmentText(message) {
|
|
|
984
1075
|
var REMINDER_EVERY_DELIVERIES = 5;
|
|
985
1076
|
var TASK_CLAIM_INSTRUCTION = "Claim this task before working it, then update task status when finished.";
|
|
986
1077
|
function stripRelayScaffolding(body) {
|
|
987
|
-
const
|
|
1078
|
+
const lines2 = body.split(`
|
|
988
1079
|
`);
|
|
989
1080
|
const popTrailingBlanks = () => {
|
|
990
|
-
while (
|
|
991
|
-
|
|
1081
|
+
while (lines2.length && (lines2.at(-1) ?? "").trim() === "")
|
|
1082
|
+
lines2.pop();
|
|
992
1083
|
};
|
|
993
1084
|
popTrailingBlanks();
|
|
994
|
-
if (
|
|
995
|
-
|
|
1085
|
+
if (lines2.at(-1) === TASK_CLAIM_INSTRUCTION) {
|
|
1086
|
+
lines2.pop();
|
|
996
1087
|
popTrailingBlanks();
|
|
997
1088
|
}
|
|
998
|
-
return
|
|
1089
|
+
return lines2.join(`
|
|
999
1090
|
`);
|
|
1000
1091
|
}
|
|
1001
1092
|
function isMemoryInjection(message) {
|
|
@@ -1100,7 +1191,7 @@ ws.onmessage = (event) => {
|
|
|
1100
1191
|
const messages2 = payload.messages;
|
|
1101
1192
|
deliveryCount += 1;
|
|
1102
1193
|
if (firstDelivery) {
|
|
1103
|
-
console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ?
|
|
1194
|
+
console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? claudeReadOnlyRelayContext() : claudeRelayContext());
|
|
1104
1195
|
const wsNote = workspaceDepsNoteFromEnv();
|
|
1105
1196
|
if (wsNote) {
|
|
1106
1197
|
console.log("");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Message } from "agent-relay-sdk";
|
|
2
2
|
import { claudeProviderMessageText } from "../../../src/adapters/claude-delivery";
|
|
3
|
-
import {
|
|
3
|
+
import { claudeReadOnlyRelayContext, claudeRelayContext, workspaceDepsNoteFromEnv } from "../../../src/relay-instructions";
|
|
4
4
|
|
|
5
5
|
const port = process.env.AGENT_RELAY_RUNNER_PORT;
|
|
6
6
|
if (!port) process.exit(0);
|
|
@@ -15,7 +15,7 @@ ws.onmessage = (event) => {
|
|
|
15
15
|
const messages = payload.messages as Message[];
|
|
16
16
|
deliveryCount += 1;
|
|
17
17
|
if (firstDelivery) {
|
|
18
|
-
console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ?
|
|
18
|
+
console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? claudeReadOnlyRelayContext() : claudeRelayContext());
|
|
19
19
|
const wsNote = workspaceDepsNoteFromEnv();
|
|
20
20
|
if (wsNote) {
|
|
21
21
|
console.log("");
|
package/src/adapter.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { LivenessInputs } from "./liveness";
|
|
|
4
4
|
import type { SessionEvent } from "./session-insights";
|
|
5
5
|
import { messageBodyMaxCharsFromEnv } from "./config";
|
|
6
6
|
import { relayReplyReminderText } from "./relay-instructions";
|
|
7
|
+
import { renderRunnerTemplate } from "./template-resolver";
|
|
7
8
|
|
|
8
9
|
export type SemanticStatus = "idle" | "busy" | "offline" | "error";
|
|
9
10
|
type ProviderWorkKind = "provider-turn" | "subagent" | "background-script";
|
|
@@ -226,7 +227,9 @@ export function profileIsVanillaBase(config: Pick<RunnerSpawnConfig, "agentProfi
|
|
|
226
227
|
return config.agentProfile?.base === "vanilla";
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
export
|
|
230
|
+
export function relayContext(): string {
|
|
231
|
+
return renderRunnerTemplate("relay.context.codex");
|
|
232
|
+
}
|
|
230
233
|
|
|
231
234
|
// #306 — deliver the FULL message body by default. Only a pathological body beyond this
|
|
232
235
|
// high cap truncates (with a get-message hint) so it can't nuke an agent's context; the 99%
|
package/src/adapters/codex.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { basename, join, resolve } from "node:path";
|
|
|
4
4
|
import type { ContextState, InteractivePrompt, LivenessSignal, Message, ProviderState } from "agent-relay-sdk";
|
|
5
5
|
import { isRecord, stringValue } from "agent-relay-sdk";
|
|
6
6
|
import { isPidAlive, killPid, processTreePids, processTreePidsFromTable, waitForPidsExit } from "agent-relay-sdk/process-utils";
|
|
7
|
-
import { profileAllowsRelayFeature, providerMessageText,
|
|
7
|
+
import { profileAllowsRelayFeature, providerMessageText, relayContext, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderSessionEvent, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
|
|
8
8
|
import { workspaceDepsNoteFromEnv } from "../relay-instructions";
|
|
9
9
|
import { tomlString } from "../relay-mcp";
|
|
10
10
|
import { assembleLaunch, bundledCodexSkillDirs, bundledSkillConfigArgs, materializeLaunchAssembly } from "../launch-assembly";
|
|
@@ -16,7 +16,7 @@ import { computeLivenessSignal, type LivenessInputs } from "../liveness";
|
|
|
16
16
|
/** Relay context prepended to a Codex agent's first turn: the standard relay
|
|
17
17
|
* blurb plus, when running in an isolated workspace, the deps caveat (#159). */
|
|
18
18
|
function codexRelayContextBlock(): string {
|
|
19
|
-
return [
|
|
19
|
+
return [relayContext(), workspaceDepsNoteFromEnv()].filter(Boolean).join("\n\n");
|
|
20
20
|
}
|
|
21
21
|
import { prepareCodexProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
|
|
22
22
|
import { CodexAppClient, type ClientEvent } from "./codex-client";
|
package/src/profile-home.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
|
5
5
|
import type { ProvisioningMcpServer, SpawnProvider } from "agent-relay-sdk";
|
|
6
6
|
import { getManifest } from "agent-relay-providers";
|
|
7
7
|
import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
|
|
8
|
-
import {
|
|
8
|
+
import { claudeRelayManual } from "./relay-instructions";
|
|
9
9
|
import { providerHomeRootFromEnv } from "./config";
|
|
10
10
|
import { applyClaudeConfigPromptGatePreventions } from "./claude-prompt-gates";
|
|
11
11
|
|
|
@@ -184,7 +184,7 @@ function trustWorkspaceForCodex(codexHome: string, config: RunnerSpawnConfig): v
|
|
|
184
184
|
|
|
185
185
|
function writeClaudeRelayManual(claudeHome: string): void {
|
|
186
186
|
const path = join(claudeHome, "CLAUDE.md");
|
|
187
|
-
writeFileSync(path,
|
|
187
|
+
writeFileSync(path, claudeRelayManual(), { mode: 0o600 });
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
function tomlBasicString(value: string): string {
|
|
@@ -1,29 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { renderRunnerTemplate } from "./template-resolver";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
- Prefer the inline MCP tools \`relay_reply\` or \`relay_send_message\` for the actual answer to an incoming Relay message.
|
|
7
|
-
- Do not send \`/message\` with the useful answer and then a separate \`/reply\` that only says it was sent.
|
|
8
|
-
- If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.
|
|
9
|
-
- If the useful response was already delivered through Relay, do not send an extra "sent", "done", or "drafts sent" confirmation unless the user explicitly asked for one.
|
|
10
|
-
- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.
|
|
11
|
-
- NEVER reply to a notification-class message. The server marks these and renders a single \`↪ Notification — no reply needed.\` line instead of the reply reminder — it is a fire-and-forget signal (a merge notice, lifecycle event, or FYI). Act on the information if relevant, but do not send any reply, status confirmation, or reaction back.
|
|
12
|
-
- Use \`agent-relay /react <messageId> <emoji>\` instead of a text reply for lightweight acknowledgement, approval, thanks, or "good job" after a completed work update.
|
|
13
|
-
- Good reaction uses: acknowledge praise with 👍 or ❤️, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.
|
|
14
|
-
- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.
|
|
15
|
-
- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.
|
|
16
|
-
- Use \`agent-relay get-message <id>\` when a delivered preview is truncated.
|
|
17
|
-
- Use \`agent-relay /status --json\` to inspect your current Relay identity and \`agent-relay /guide\` for command details.
|
|
18
|
-
`;
|
|
19
|
-
|
|
20
|
-
export const CLAUDE_RELAY_CONTEXT = `[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.`;
|
|
21
|
-
|
|
22
|
-
export const CLAUDE_READ_ONLY_RELAY_CONTEXT = `${CLAUDE_RELAY_CONTEXT}
|
|
3
|
+
export function claudeRelayManual(): string {
|
|
4
|
+
return renderRunnerTemplate("relay.manual.claude");
|
|
5
|
+
}
|
|
23
6
|
|
|
24
|
-
|
|
7
|
+
export function claudeRelayContext(): string {
|
|
8
|
+
return renderRunnerTemplate("relay.context.claude");
|
|
9
|
+
}
|
|
25
10
|
|
|
26
|
-
|
|
11
|
+
export function claudeReadOnlyRelayContext(): string {
|
|
12
|
+
return renderRunnerTemplate("relay.context.claude.readonly");
|
|
13
|
+
}
|
|
27
14
|
|
|
28
15
|
export function relayReplyActionText(messageId: number): string {
|
|
29
16
|
return `relay_reply (messageId: ${messageId}, body: "<your reply>"). CLI fallback: agent-relay /reply ${messageId} "<your reply>"`;
|
|
@@ -33,59 +20,30 @@ export function relayReplyReminderText(messageId: number): string {
|
|
|
33
20
|
return `If this batch needs a response, send one useful reply to the latest relevant message with ${relayReplyActionText(messageId)}`;
|
|
34
21
|
}
|
|
35
22
|
|
|
36
|
-
/**
|
|
37
|
-
* Provider-agnostic caveat injected into every spawned agent that runs in an
|
|
38
|
-
* isolated workspace, regardless of which project it is working in. Isolated
|
|
39
|
-
* workspaces are git worktrees whose node_modules are provisioned by the
|
|
40
|
-
* orchestrator (see AGENT_RELAY_WORKSPACE_DEPS): symlinked from the main
|
|
41
|
-
* checkout by default. The agent needs to know this so it doesn't run a clean
|
|
42
|
-
* install that mutates the shared node_modules. Returns "" when no note applies
|
|
43
|
-
* (shared workspace, or deps installed fresh / unknown).
|
|
44
|
-
*/
|
|
45
23
|
export function workspaceDepsNote(input: { mode?: string | null; depsMode?: string | null }): string {
|
|
46
24
|
if (input.mode !== "isolated") return "";
|
|
47
25
|
switch (input.depsMode) {
|
|
48
26
|
case "symlink":
|
|
49
|
-
return "
|
|
27
|
+
return renderRunnerTemplate("workspace.deps.symlink");
|
|
50
28
|
case "none":
|
|
51
|
-
return "
|
|
29
|
+
return renderRunnerTemplate("workspace.deps.none");
|
|
52
30
|
default:
|
|
53
31
|
return "";
|
|
54
32
|
}
|
|
55
33
|
}
|
|
56
34
|
|
|
57
|
-
/**
|
|
58
|
-
* Lifecycle briefing for an agent spawned into an isolated workspace (#205).
|
|
59
|
-
* Tells it, in plain terms, that it is on an isolated branch off a base that
|
|
60
|
-
* other agents move in parallel, and how to hand off — so it doesn't re-derive
|
|
61
|
-
* "I seem to be in a clone, not on main" every session or hand-roll a rebase/push.
|
|
62
|
-
* Returns "" for shared workspaces. Branch/base come from the resolved metadata.
|
|
63
|
-
*/
|
|
64
35
|
export function workspaceLifecycleNote(input: { mode?: string | null; branch?: string | null; baseRef?: string | null }): string {
|
|
65
36
|
if (input.mode !== "isolated") return "";
|
|
66
37
|
const branch = input.branch ? `\`${input.branch}\`` : "an isolated agent branch";
|
|
67
38
|
const base = input.baseRef ? `\`${input.baseRef}\`` : "the base branch";
|
|
68
|
-
return
|
|
69
|
-
`[agent-relay] Isolated workspace: you are in a git worktree on branch ${branch}, based on ${base} — NOT the main checkout. Other agents may work in parallel and land to ${base}, so ${base} will move under you. That is expected; don't fight it.`,
|
|
70
|
-
`Do NOT push this branch yourself — not with \`git push\`, not with \`tl push\` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run \`agent-relay workspace ready\` — Relay rebases onto the latest ${base}, lands your work, and pushes for you. If the installed \`agent-relay\` binary is stale and says the workspace command is unknown, run the repo-local fallback: \`bun src/index.ts workspace ready\`.`,
|
|
71
|
-
`After \`ready\`, the status becomes \`review_requested\` — this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after \`ready\` and go idle; do NOT wait or poll the steward queue. Relay wakes you with \`landed-success\` when your branch lands and refreshes, or \`landed-failure\` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a \`--N\` suffix — expected, keep working there. Never \`cd\` into the main checkout, and never merge/push/resolve conflicts yourself — Relay and the steward own all of that. \`agent-relay workspace status\` anytime shows your current state and the exact next step.`,
|
|
72
|
-
].join("\n");
|
|
39
|
+
return renderRunnerTemplate("workspace.lifecycle", { branch, baseRef: base });
|
|
73
40
|
}
|
|
74
41
|
|
|
75
|
-
/**
|
|
76
|
-
* Caveat for untracked paths symlinked from main into an isolated worktree
|
|
77
|
-
* (WorkspaceConfig.symlinkPaths, e.g. AGENTS.md, .claude-rig). Edits to these
|
|
78
|
-
* write THROUGH to the main checkout — the agent must know so it doesn't mutate
|
|
79
|
-
* shared config thinking it's worktree-local. Returns "" when nothing was linked.
|
|
80
|
-
*/
|
|
81
42
|
export function workspaceSymlinksNote(linked: string[]): string {
|
|
82
43
|
if (!linked.length) return "";
|
|
83
|
-
return
|
|
44
|
+
return renderRunnerTemplate("workspace.symlinks", { linked: linked.join(", ") });
|
|
84
45
|
}
|
|
85
46
|
|
|
86
|
-
/** Resolve the workspace caveats from the runner/monitor environment.
|
|
87
|
-
* AGENT_RELAY_WORKSPACE_JSON carries the resolved workspace metadata (mode +
|
|
88
|
-
* deps + symlinks) and is the authoritative source. Best-effort: never throws. */
|
|
89
47
|
export function workspaceDepsNoteFromEnv(env: Record<string, string | undefined> = process.env): string {
|
|
90
48
|
const json = env.AGENT_RELAY_WORKSPACE_JSON;
|
|
91
49
|
if (!json) return "";
|
package/src/runner-core.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { providerTerminalSession, providerTerminalSocket } from "./process-meta"
|
|
|
33
33
|
import { capsFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
|
|
34
34
|
import { boundaryReasonForCommand, PRE_DESTROY_TIMEOUT_MS, reasonExitsRunner, type LifecycleAction, type SessionDestroyReason } from "./session-destroy";
|
|
35
35
|
import { isNativeSelfResumeCompact, NativeSelfResumeTracker, recoverNativeSelfResumeExit } from "./native-self-resume";
|
|
36
|
+
import { initializeRunnerTemplates } from "./template-resolver";
|
|
36
37
|
import {
|
|
37
38
|
appliedAgentProfileMetadata,
|
|
38
39
|
commandTimeoutMs,
|
|
@@ -442,7 +443,7 @@ export class AgentRunner {
|
|
|
442
443
|
void this.handleCommand(type, params, commandId, command);
|
|
443
444
|
});
|
|
444
445
|
this.bus.on("error", (code, message) => this.handleBusError(String(code), String(message)));
|
|
445
|
-
await registerWithinDeadline(this.bus, registrationTimeoutMsFromEnv(), this.options.relayUrl, (reason) => logger.fatal("register", reason));
|
|
446
|
+
await registerWithinDeadline(this.bus, registrationTimeoutMsFromEnv(), this.options.relayUrl, (reason) => logger.fatal("register", reason)); await initializeRunnerTemplates(this.http);
|
|
446
447
|
this.obligationCache.start();
|
|
447
448
|
this.outbox.start();
|
|
448
449
|
this.sessionOutbox.start();
|
|
@@ -487,8 +488,7 @@ export class AgentRunner {
|
|
|
487
488
|
this.busyReconciler.disarm();
|
|
488
489
|
this.stopReasoningTail();
|
|
489
490
|
this.obligationCache.stop();
|
|
490
|
-
this.outbox.close();
|
|
491
|
-
this.sessionOutbox.close();
|
|
491
|
+
this.outbox.close(); this.sessionOutbox.close();
|
|
492
492
|
this.proxy?.stop();
|
|
493
493
|
this.control?.stop();
|
|
494
494
|
await this.bus.close();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RelayHttpClient } from "agent-relay-sdk";
|
|
2
|
+
import { RUNNER_PROMPT_TEMPLATE_DEFAULTS } from "agent-relay-sdk/prompt-template-defaults";
|
|
3
|
+
|
|
4
|
+
const RUNNER_DEFAULTS = new Map<string, string>(RUNNER_PROMPT_TEMPLATE_DEFAULTS.map((template) => [template.slug, template.defaultTemplate]));
|
|
5
|
+
const templateCache = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
function renderValue(value: string | undefined): string {
|
|
8
|
+
return value ?? "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function applyVariables(template: string, vars: Record<string, string>): string {
|
|
12
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => renderValue(vars[key]));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function initializeRunnerTemplates(http: RelayHttpClient): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
const templates = await http.listPromptTemplates();
|
|
18
|
+
templateCache.clear();
|
|
19
|
+
for (const template of templates) templateCache.set(template.slug, template.template);
|
|
20
|
+
} catch {
|
|
21
|
+
templateCache.clear();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderRunnerTemplate(slug: string, vars: Record<string, string> = {}): string {
|
|
26
|
+
const template = templateCache.get(slug) ?? RUNNER_DEFAULTS.get(slug);
|
|
27
|
+
if (template === undefined) throw new Error(`unknown runner prompt template slug: ${slug}`);
|
|
28
|
+
return applyVariables(template, vars);
|
|
29
|
+
}
|