@agnishc/edb-subagents 0.10.8 → 0.10.9
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/CHANGELOG.md +2 -0
- package/package.json +1 -1
- package/src/agent-types.test.ts +126 -0
- package/src/memory.test.ts +40 -0
- package/src/prompts.test.ts +76 -0
- package/src/usage.test.ts +77 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agnishc/edb-subagents",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.9",
|
|
4
4
|
"description": "Pi extension: Claude Code-style autonomous sub-agents with live widget, parallel execution, mid-run steering, and custom agent types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { getAgentConfig, getAvailableTypes, getToolNamesForType, registerAgents, resolveType } from "./agent-types.js";
|
|
3
|
+
import type { AgentConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("agent-types registry", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Reset registry by re-registering empty map
|
|
8
|
+
registerAgents(new Map());
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("registerAgents adds types to registry", () => {
|
|
12
|
+
const config: AgentConfig = {
|
|
13
|
+
name: "test",
|
|
14
|
+
description: "Test agent",
|
|
15
|
+
model: "test/model",
|
|
16
|
+
extensions: true,
|
|
17
|
+
skills: true,
|
|
18
|
+
promptMode: "append",
|
|
19
|
+
systemPrompt: "Test",
|
|
20
|
+
};
|
|
21
|
+
registerAgents(new Map([["test", config]]));
|
|
22
|
+
expect(getAvailableTypes()).toContain("test");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("resolveType returns config for registered type", () => {
|
|
26
|
+
const config: AgentConfig = {
|
|
27
|
+
name: "coder",
|
|
28
|
+
description: "Coder agent",
|
|
29
|
+
model: "test/model",
|
|
30
|
+
extensions: true,
|
|
31
|
+
skills: true,
|
|
32
|
+
promptMode: "append",
|
|
33
|
+
systemPrompt: "You are a coder",
|
|
34
|
+
};
|
|
35
|
+
registerAgents(new Map([["coder", config]]));
|
|
36
|
+
const resolved = resolveType("coder");
|
|
37
|
+
expect(resolved).toBe("coder");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("resolveType is case-insensitive", () => {
|
|
41
|
+
const config: AgentConfig = {
|
|
42
|
+
name: "Coder",
|
|
43
|
+
description: "Coder",
|
|
44
|
+
model: "m",
|
|
45
|
+
extensions: true,
|
|
46
|
+
skills: true,
|
|
47
|
+
promptMode: "append",
|
|
48
|
+
systemPrompt: "Test",
|
|
49
|
+
};
|
|
50
|
+
registerAgents(new Map([["Coder", config]]));
|
|
51
|
+
expect(resolveType("CODER")).toBe("Coder");
|
|
52
|
+
expect(resolveType("coder")).toBe("Coder");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("resolveType returns undefined for unknown type", () => {
|
|
56
|
+
registerAgents(new Map());
|
|
57
|
+
expect(resolveType("nonexistent")).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("getAgentConfig returns config for registered type", () => {
|
|
61
|
+
const config: AgentConfig = {
|
|
62
|
+
name: "reviewer",
|
|
63
|
+
description: "Reviewer",
|
|
64
|
+
model: "test/model",
|
|
65
|
+
extensions: true,
|
|
66
|
+
skills: true,
|
|
67
|
+
promptMode: "append",
|
|
68
|
+
systemPrompt: "You are a reviewer",
|
|
69
|
+
};
|
|
70
|
+
registerAgents(new Map([["reviewer", config]]));
|
|
71
|
+
const result = getAgentConfig("reviewer");
|
|
72
|
+
expect(result?.name).toBe("reviewer");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("getAgentConfig returns undefined for unknown type", () => {
|
|
76
|
+
registerAgents(new Map());
|
|
77
|
+
expect(getAgentConfig("nonexistent")).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("getAvailableTypes excludes disabled agents", () => {
|
|
81
|
+
const config: AgentConfig = {
|
|
82
|
+
name: "disabled",
|
|
83
|
+
description: "Disabled",
|
|
84
|
+
model: "test/model",
|
|
85
|
+
enabled: false,
|
|
86
|
+
extensions: true,
|
|
87
|
+
skills: true,
|
|
88
|
+
promptMode: "append",
|
|
89
|
+
systemPrompt: "Disabled",
|
|
90
|
+
};
|
|
91
|
+
registerAgents(new Map([["disabled", config]]));
|
|
92
|
+
expect(getAvailableTypes()).not.toContain("disabled");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("getToolNamesForType returns tools for registered type", () => {
|
|
96
|
+
const config: AgentConfig = {
|
|
97
|
+
name: "coder",
|
|
98
|
+
description: "Coder",
|
|
99
|
+
model: "m",
|
|
100
|
+
builtinToolNames: ["read", "write", "bash"],
|
|
101
|
+
extensions: true,
|
|
102
|
+
skills: true,
|
|
103
|
+
promptMode: "append",
|
|
104
|
+
systemPrompt: "Test",
|
|
105
|
+
};
|
|
106
|
+
registerAgents(new Map([["coder", config]]));
|
|
107
|
+
const tools = getToolNamesForType("coder");
|
|
108
|
+
expect(tools).toContain("read");
|
|
109
|
+
expect(tools).toContain("write");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("getToolNamesForType returns default tools when not specified", () => {
|
|
113
|
+
const config: AgentConfig = {
|
|
114
|
+
name: "custom",
|
|
115
|
+
description: "Custom",
|
|
116
|
+
model: "m",
|
|
117
|
+
extensions: true,
|
|
118
|
+
skills: true,
|
|
119
|
+
promptMode: "append",
|
|
120
|
+
systemPrompt: "Test",
|
|
121
|
+
};
|
|
122
|
+
registerAgents(new Map([["custom", config]]));
|
|
123
|
+
const tools = getToolNamesForType("custom");
|
|
124
|
+
expect(tools).toContain("read");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isUnsafeName } from "./memory.js";
|
|
3
|
+
|
|
4
|
+
describe("isUnsafeName", () => {
|
|
5
|
+
it("returns false for safe names", () => {
|
|
6
|
+
expect(isUnsafeName("index")).toBe(false);
|
|
7
|
+
expect(isUnsafeName("index.ts")).toBe(false);
|
|
8
|
+
expect(isUnsafeName("my-agent")).toBe(false);
|
|
9
|
+
expect(isUnsafeName("my_agent_123")).toBe(false);
|
|
10
|
+
expect(isUnsafeName("Agent2")).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns true for empty name", () => {
|
|
14
|
+
expect(isUnsafeName("")).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns true for names over 128 chars", () => {
|
|
18
|
+
expect(isUnsafeName("a".repeat(129))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns true for names starting with dot", () => {
|
|
22
|
+
expect(isUnsafeName(".git")).toBe(true);
|
|
23
|
+
expect(isUnsafeName(".DS_Store")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("allows underscores in safe names", () => {
|
|
27
|
+
// The regex allows underscores, so node_modules is technically "safe"
|
|
28
|
+
expect(isUnsafeName("node_modules")).toBe(false);
|
|
29
|
+
expect(isUnsafeName("my_agent")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns true for names with special characters", () => {
|
|
33
|
+
expect(isUnsafeName("agent@home")).toBe(true);
|
|
34
|
+
expect(isUnsafeName("agent space")).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns true for leading hyphen", () => {
|
|
38
|
+
expect(isUnsafeName("-agent")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildAgentPrompt } from "./prompts.js";
|
|
3
|
+
import type { AgentConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("buildAgentPrompt", () => {
|
|
6
|
+
const baseConfig: AgentConfig = {
|
|
7
|
+
name: "Test Agent",
|
|
8
|
+
description: "Test agent",
|
|
9
|
+
extensions: true,
|
|
10
|
+
skills: true,
|
|
11
|
+
promptMode: "replace",
|
|
12
|
+
systemPrompt: "You are a test agent.",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const baseEnv = {
|
|
16
|
+
cwd: "/tmp/test",
|
|
17
|
+
platform: "darwin",
|
|
18
|
+
isGitRepo: false,
|
|
19
|
+
branch: "",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
it("includes active_agent tag", () => {
|
|
23
|
+
const result = buildAgentPrompt(baseConfig, baseEnv.cwd, baseEnv);
|
|
24
|
+
expect(result).toContain('<active_agent name="Test Agent"/>');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("includes environment info", () => {
|
|
28
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv);
|
|
29
|
+
expect(result).toContain("Working directory: /tmp/test");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("includes platform in replace mode", () => {
|
|
33
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv);
|
|
34
|
+
expect(result).toContain("Platform: darwin");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("includes system prompt in replace mode", () => {
|
|
38
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv);
|
|
39
|
+
expect(result).toContain("You are a test agent.");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("includes git info when in git repo", () => {
|
|
43
|
+
const env = { ...baseEnv, isGitRepo: true, branch: "main" };
|
|
44
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", env);
|
|
45
|
+
expect(result).toContain("Git repository: yes");
|
|
46
|
+
expect(result).toContain("Branch: main");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("includes parent system prompt in append mode", () => {
|
|
50
|
+
const config: AgentConfig = { ...baseConfig, promptMode: "append" };
|
|
51
|
+
const result = buildAgentPrompt(config, "/tmp/test", baseEnv, "You are the parent.");
|
|
52
|
+
expect(result).toContain("You are the parent.");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("includes agent instructions in append mode", () => {
|
|
56
|
+
const config: AgentConfig = { ...baseConfig, promptMode: "append" };
|
|
57
|
+
const result = buildAgentPrompt(config, "/tmp/test", baseEnv);
|
|
58
|
+
expect(result).toContain("<agent_instructions>");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("includes memory block when provided", () => {
|
|
62
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv, undefined, {
|
|
63
|
+
memoryBlock: "# Memory\nPrevious work: done",
|
|
64
|
+
});
|
|
65
|
+
expect(result).toContain("Memory");
|
|
66
|
+
expect(result).toContain("Previous work: done");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes skill blocks when provided", () => {
|
|
70
|
+
const result = buildAgentPrompt(baseConfig, "/tmp/test", baseEnv, undefined, {
|
|
71
|
+
skillBlocks: [{ name: "TestSkill", content: "Test content" }],
|
|
72
|
+
});
|
|
73
|
+
expect(result).toContain("Preloaded Skill: TestSkill");
|
|
74
|
+
expect(result).toContain("Test content");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { LifetimeUsage } from "./usage.js";
|
|
3
|
+
import { getLifetimeTotal, getSessionContextPercent, getSessionTokens } from "./usage.js";
|
|
4
|
+
|
|
5
|
+
describe("getLifetimeTotal", () => {
|
|
6
|
+
it("returns 0 when undefined", () => {
|
|
7
|
+
expect(getLifetimeTotal(undefined)).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("sums tokens from usage object", () => {
|
|
11
|
+
const usage: LifetimeUsage = { input: 100, output: 200, cacheWrite: 50 };
|
|
12
|
+
expect(getLifetimeTotal(usage)).toBe(350);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("handles single component", () => {
|
|
16
|
+
const usage: LifetimeUsage = { input: 500, output: 0, cacheWrite: 0 };
|
|
17
|
+
expect(getLifetimeTotal(usage)).toBe(500);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("getSessionTokens", () => {
|
|
22
|
+
it("returns 0 for undefined session", () => {
|
|
23
|
+
expect(getSessionTokens(undefined)).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns 0 when getSessionStats throws", () => {
|
|
27
|
+
const badSession = {
|
|
28
|
+
getSessionStats: () => {
|
|
29
|
+
throw new Error("no stats");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
expect(getSessionTokens(badSession as any)).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("sums tokens from session stats", () => {
|
|
36
|
+
const session = {
|
|
37
|
+
getSessionStats: () => ({
|
|
38
|
+
tokens: { input: 100, output: 200, cacheWrite: 50 },
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
expect(getSessionTokens(session as any)).toBe(350);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("getSessionContextPercent", () => {
|
|
46
|
+
it("returns null for undefined session", () => {
|
|
47
|
+
expect(getSessionContextPercent(undefined)).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns null when getSessionStats throws", () => {
|
|
51
|
+
const badSession = {
|
|
52
|
+
getSessionStats: () => {
|
|
53
|
+
throw new Error("no stats");
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
expect(getSessionContextPercent(badSession as any)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns context percent from session stats", () => {
|
|
60
|
+
const session = {
|
|
61
|
+
getSessionStats: () => ({
|
|
62
|
+
tokens: { input: 100, output: 200, cacheWrite: 0 },
|
|
63
|
+
contextUsage: { percent: 75 },
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
expect(getSessionContextPercent(session as any)).toBe(75);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns null when percent is missing", () => {
|
|
70
|
+
const session = {
|
|
71
|
+
getSessionStats: () => ({
|
|
72
|
+
tokens: { input: 100, output: 200, cacheWrite: 0 },
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
expect(getSessionContextPercent(session as any)).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|