Skip to content

Commit eed403d

Browse files
committed
refactor(agents): unify spawned metadata and extract attachments service
1 parent 61000b8 commit eed403d

9 files changed

Lines changed: 675 additions & 350 deletions

File tree

src/agents/openclaw-tools.ts

Lines changed: 47 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { resolvePluginTools } from "../plugins/tools.js";
33
import type { GatewayMessageChannel } from "../utils/message-channel.js";
44
import { resolveSessionAgentId } from "./agent-scope.js";
55
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
6+
import type { SpawnedToolContext } from "./spawned-context.js";
67
import type { ToolFsPolicy } from "./tool-fs-policy.js";
78
import { createAgentsListTool } from "./tools/agents-list-tool.js";
89
import { createBrowserTool } from "./tools/browser-tool.js";
@@ -24,57 +25,52 @@ import { createTtsTool } from "./tools/tts-tool.js";
2425
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
2526
import { resolveWorkspaceRoot } from "./workspace-dir.js";
2627

27-
export function createOpenClawTools(options?: {
28-
sandboxBrowserBridgeUrl?: string;
29-
allowHostBrowserControl?: boolean;
30-
agentSessionKey?: string;
31-
agentChannel?: GatewayMessageChannel;
32-
agentAccountId?: string;
33-
/** Delivery target (e.g. telegram:group:123:topic:456) for topic/thread routing. */
34-
agentTo?: string;
35-
/** Thread/topic identifier for routing replies to the originating thread. */
36-
agentThreadId?: string | number;
37-
/** Group id for channel-level tool policy inheritance. */
38-
agentGroupId?: string | null;
39-
/** Group channel label for channel-level tool policy inheritance. */
40-
agentGroupChannel?: string | null;
41-
/** Group space label for channel-level tool policy inheritance. */
42-
agentGroupSpace?: string | null;
43-
agentDir?: string;
44-
sandboxRoot?: string;
45-
sandboxFsBridge?: SandboxFsBridge;
46-
fsPolicy?: ToolFsPolicy;
47-
workspaceDir?: string;
48-
sandboxed?: boolean;
49-
config?: OpenClawConfig;
50-
pluginToolAllowlist?: string[];
51-
/** Current channel ID for auto-threading (Slack). */
52-
currentChannelId?: string;
53-
/** Current thread timestamp for auto-threading (Slack). */
54-
currentThreadTs?: string;
55-
/** Current inbound message id for action fallbacks (e.g. Telegram react). */
56-
currentMessageId?: string | number;
57-
/** Reply-to mode for Slack auto-threading. */
58-
replyToMode?: "off" | "first" | "all";
59-
/** Mutable ref to track if a reply was sent (for "first" mode). */
60-
hasRepliedRef?: { value: boolean };
61-
/** If true, the model has native vision capability */
62-
modelHasVision?: boolean;
63-
/** If true, nodes action="invoke" can call media-returning commands directly. */
64-
allowMediaInvokeCommands?: boolean;
65-
/** Explicit agent ID override for cron/hook sessions. */
66-
requesterAgentIdOverride?: string;
67-
/** Require explicit message targets (no implicit last-route sends). */
68-
requireExplicitMessageTarget?: boolean;
69-
/** If true, omit the message tool from the tool list. */
70-
disableMessageTool?: boolean;
71-
/** Trusted sender id from inbound context (not tool args). */
72-
requesterSenderId?: string | null;
73-
/** Whether the requesting sender is an owner. */
74-
senderIsOwner?: boolean;
75-
/** Ephemeral session UUID — regenerated on /new and /reset. */
76-
sessionId?: string;
77-
}): AnyAgentTool[] {
28+
export function createOpenClawTools(
29+
options?: {
30+
sandboxBrowserBridgeUrl?: string;
31+
allowHostBrowserControl?: boolean;
32+
agentSessionKey?: string;
33+
agentChannel?: GatewayMessageChannel;
34+
agentAccountId?: string;
35+
/** Delivery target (e.g. telegram:group:123:topic:456) for topic/thread routing. */
36+
agentTo?: string;
37+
/** Thread/topic identifier for routing replies to the originating thread. */
38+
agentThreadId?: string | number;
39+
agentDir?: string;
40+
sandboxRoot?: string;
41+
sandboxFsBridge?: SandboxFsBridge;
42+
fsPolicy?: ToolFsPolicy;
43+
sandboxed?: boolean;
44+
config?: OpenClawConfig;
45+
pluginToolAllowlist?: string[];
46+
/** Current channel ID for auto-threading (Slack). */
47+
currentChannelId?: string;
48+
/** Current thread timestamp for auto-threading (Slack). */
49+
currentThreadTs?: string;
50+
/** Current inbound message id for action fallbacks (e.g. Telegram react). */
51+
currentMessageId?: string | number;
52+
/** Reply-to mode for Slack auto-threading. */
53+
replyToMode?: "off" | "first" | "all";
54+
/** Mutable ref to track if a reply was sent (for "first" mode). */
55+
hasRepliedRef?: { value: boolean };
56+
/** If true, the model has native vision capability */
57+
modelHasVision?: boolean;
58+
/** If true, nodes action="invoke" can call media-returning commands directly. */
59+
allowMediaInvokeCommands?: boolean;
60+
/** Explicit agent ID override for cron/hook sessions. */
61+
requesterAgentIdOverride?: string;
62+
/** Require explicit message targets (no implicit last-route sends). */
63+
requireExplicitMessageTarget?: boolean;
64+
/** If true, omit the message tool from the tool list. */
65+
disableMessageTool?: boolean;
66+
/** Trusted sender id from inbound context (not tool args). */
67+
requesterSenderId?: string | null;
68+
/** Whether the requesting sender is an owner. */
69+
senderIsOwner?: boolean;
70+
/** Ephemeral session UUID — regenerated on /new and /reset. */
71+
sessionId?: string;
72+
} & SpawnedToolContext,
73+
): AnyAgentTool[] {
7874
const workspaceDir = resolveWorkspaceRoot(options?.workspaceDir);
7975
const imageTool = options?.agentDir?.trim()
8076
? createImageTool({

src/agents/spawned-context.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
mapToolContextToSpawnedRunMetadata,
4+
normalizeSpawnedRunMetadata,
5+
resolveIngressWorkspaceOverrideForSpawnedRun,
6+
resolveSpawnedWorkspaceInheritance,
7+
} from "./spawned-context.js";
8+
9+
describe("normalizeSpawnedRunMetadata", () => {
10+
it("trims text fields and drops empties", () => {
11+
expect(
12+
normalizeSpawnedRunMetadata({
13+
spawnedBy: " agent:main:subagent:1 ",
14+
groupId: " group-1 ",
15+
groupChannel: " slack ",
16+
groupSpace: " ",
17+
workspaceDir: " /tmp/ws ",
18+
}),
19+
).toEqual({
20+
spawnedBy: "agent:main:subagent:1",
21+
groupId: "group-1",
22+
groupChannel: "slack",
23+
workspaceDir: "/tmp/ws",
24+
});
25+
});
26+
});
27+
28+
describe("mapToolContextToSpawnedRunMetadata", () => {
29+
it("maps agent group fields to run metadata shape", () => {
30+
expect(
31+
mapToolContextToSpawnedRunMetadata({
32+
agentGroupId: "g-1",
33+
agentGroupChannel: "telegram",
34+
agentGroupSpace: "topic:123",
35+
workspaceDir: "/tmp/ws",
36+
}),
37+
).toEqual({
38+
groupId: "g-1",
39+
groupChannel: "telegram",
40+
groupSpace: "topic:123",
41+
workspaceDir: "/tmp/ws",
42+
});
43+
});
44+
});
45+
46+
describe("resolveSpawnedWorkspaceInheritance", () => {
47+
it("prefers explicit workspaceDir when provided", () => {
48+
const resolved = resolveSpawnedWorkspaceInheritance({
49+
config: {},
50+
requesterSessionKey: "agent:main:subagent:parent",
51+
explicitWorkspaceDir: " /tmp/explicit ",
52+
});
53+
expect(resolved).toBe("/tmp/explicit");
54+
});
55+
56+
it("returns undefined for missing requester context", () => {
57+
const resolved = resolveSpawnedWorkspaceInheritance({
58+
config: {},
59+
requesterSessionKey: undefined,
60+
explicitWorkspaceDir: undefined,
61+
});
62+
expect(resolved).toBeUndefined();
63+
});
64+
});
65+
66+
describe("resolveIngressWorkspaceOverrideForSpawnedRun", () => {
67+
it("forwards workspace only for spawned runs", () => {
68+
expect(
69+
resolveIngressWorkspaceOverrideForSpawnedRun({
70+
spawnedBy: "agent:main:subagent:parent",
71+
workspaceDir: "/tmp/ws",
72+
}),
73+
).toBe("/tmp/ws");
74+
expect(
75+
resolveIngressWorkspaceOverrideForSpawnedRun({
76+
spawnedBy: "",
77+
workspaceDir: "/tmp/ws",
78+
}),
79+
).toBeUndefined();
80+
});
81+
});

src/agents/spawned-context.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { OpenClawConfig } from "../config/config.js";
2+
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
3+
import { resolveAgentWorkspaceDir } from "./agent-scope.js";
4+
5+
export type SpawnedRunMetadata = {
6+
spawnedBy?: string | null;
7+
groupId?: string | null;
8+
groupChannel?: string | null;
9+
groupSpace?: string | null;
10+
workspaceDir?: string | null;
11+
};
12+
13+
export type SpawnedToolContext = {
14+
agentGroupId?: string | null;
15+
agentGroupChannel?: string | null;
16+
agentGroupSpace?: string | null;
17+
workspaceDir?: string;
18+
};
19+
20+
export type NormalizedSpawnedRunMetadata = {
21+
spawnedBy?: string;
22+
groupId?: string;
23+
groupChannel?: string;
24+
groupSpace?: string;
25+
workspaceDir?: string;
26+
};
27+
28+
function normalizeOptionalText(value?: string | null): string | undefined {
29+
if (typeof value !== "string") {
30+
return undefined;
31+
}
32+
const trimmed = value.trim();
33+
return trimmed || undefined;
34+
}
35+
36+
export function normalizeSpawnedRunMetadata(
37+
value?: SpawnedRunMetadata | null,
38+
): NormalizedSpawnedRunMetadata {
39+
return {
40+
spawnedBy: normalizeOptionalText(value?.spawnedBy),
41+
groupId: normalizeOptionalText(value?.groupId),
42+
groupChannel: normalizeOptionalText(value?.groupChannel),
43+
groupSpace: normalizeOptionalText(value?.groupSpace),
44+
workspaceDir: normalizeOptionalText(value?.workspaceDir),
45+
};
46+
}
47+
48+
export function mapToolContextToSpawnedRunMetadata(
49+
value?: SpawnedToolContext | null,
50+
): Pick<NormalizedSpawnedRunMetadata, "groupId" | "groupChannel" | "groupSpace" | "workspaceDir"> {
51+
return {
52+
groupId: normalizeOptionalText(value?.agentGroupId),
53+
groupChannel: normalizeOptionalText(value?.agentGroupChannel),
54+
groupSpace: normalizeOptionalText(value?.agentGroupSpace),
55+
workspaceDir: normalizeOptionalText(value?.workspaceDir),
56+
};
57+
}
58+
59+
export function resolveSpawnedWorkspaceInheritance(params: {
60+
config: OpenClawConfig;
61+
requesterSessionKey?: string;
62+
explicitWorkspaceDir?: string | null;
63+
}): string | undefined {
64+
const explicit = normalizeOptionalText(params.explicitWorkspaceDir);
65+
if (explicit) {
66+
return explicit;
67+
}
68+
const requesterAgentId = params.requesterSessionKey
69+
? parseAgentSessionKey(params.requesterSessionKey)?.agentId
70+
: undefined;
71+
return requesterAgentId
72+
? resolveAgentWorkspaceDir(params.config, normalizeAgentId(requesterAgentId))
73+
: undefined;
74+
}
75+
76+
export function resolveIngressWorkspaceOverrideForSpawnedRun(
77+
metadata?: Pick<SpawnedRunMetadata, "spawnedBy" | "workspaceDir"> | null,
78+
): string | undefined {
79+
const normalized = normalizeSpawnedRunMetadata(metadata);
80+
return normalized.spawnedBy ? normalized.workspaceDir : undefined;
81+
}

0 commit comments

Comments
 (0)