@kevin5251984/guild 0.2.12

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/bin/guildd.mjs +20 -0
  3. package/cordis.yml +24 -0
  4. package/package.json +52 -0
  5. package/src/agent-file.ts +125 -0
  6. package/src/browser.ts +668 -0
  7. package/src/catalog/default-bots.ts +263 -0
  8. package/src/catalog/skills.ts +128 -0
  9. package/src/catalog/subagents.ts +70 -0
  10. package/src/chat-parts.ts +71 -0
  11. package/src/cli-args.ts +75 -0
  12. package/src/cli.ts +60 -0
  13. package/src/compact.ts +355 -0
  14. package/src/cordis.d.ts +40 -0
  15. package/src/db.ts +653 -0
  16. package/src/generate.ts +673 -0
  17. package/src/handlers.ts +1623 -0
  18. package/src/harness.ts +326 -0
  19. package/src/host-agents.ts +137 -0
  20. package/src/host-browse.ts +199 -0
  21. package/src/host-skills.ts +150 -0
  22. package/src/image-gen.ts +270 -0
  23. package/src/index.ts +12 -0
  24. package/src/llm.ts +993 -0
  25. package/src/mcp.ts +563 -0
  26. package/src/memory.ts +159 -0
  27. package/src/mention.ts +176 -0
  28. package/src/oauth.ts +1474 -0
  29. package/src/plugins/api.ts +8 -0
  30. package/src/plugins/chat.ts +31 -0
  31. package/src/plugins/harness.ts +77 -0
  32. package/src/plugins/llm.ts +50 -0
  33. package/src/plugins/mcp.ts +58 -0
  34. package/src/plugins/memory.ts +42 -0
  35. package/src/plugins/oauth.ts +47 -0
  36. package/src/plugins/server.ts +126 -0
  37. package/src/plugins/store.ts +29 -0
  38. package/src/plugins/tools.ts +79 -0
  39. package/src/public/buddy.js +432 -0
  40. package/src/public/chat.css +3045 -0
  41. package/src/public/chat.html +5834 -0
  42. package/src/public/favicon-16.png +0 -0
  43. package/src/public/favicon-16.svg +10 -0
  44. package/src/public/favicon-32.png +0 -0
  45. package/src/public/favicon.ico +0 -0
  46. package/src/public/favicon.svg +13 -0
  47. package/src/public/i18n.js +663 -0
  48. package/src/public/index.html +143 -0
  49. package/src/public/library.html +678 -0
  50. package/src/public/mcp-add.html +126 -0
  51. package/src/public/md.js +332 -0
  52. package/src/public/rpg/inn-street.jpg +0 -0
  53. package/src/public/settings.html +795 -0
  54. package/src/public/skills-add.html +212 -0
  55. package/src/public/studio.html +1181 -0
  56. package/src/public/style.css +1678 -0
  57. package/src/public/subagents-add.html +152 -0
  58. package/src/router.ts +978 -0
  59. package/src/send-budget.ts +52 -0
  60. package/src/server.ts +1 -0
  61. package/src/skill-import.ts +250 -0
  62. package/src/slash.ts +15 -0
  63. package/src/start.ts +103 -0
  64. package/src/store.ts +1208 -0
  65. package/src/subagent.ts +355 -0
  66. package/src/tools.ts +818 -0
  67. package/src/trajectory.ts +339 -0
  68. package/src/usage.ts +111 -0
  69. package/vendor/protocol/package.json +19 -0
  70. package/vendor/protocol/src/index.ts +159 -0
package/src/compact.ts ADDED
@@ -0,0 +1,355 @@
1
+ import type { ChatPart, ModelRef } from "@guild/protocol";
2
+ import { llmComplete } from "./llm.ts";
3
+
4
+ /** Cheap char/4 estimate, same ballpark Codex uses before a real tokenizer. */
5
+ export const CHARS_PER_TOKEN = 4;
6
+ /** Default working window minus output/tool reserve. */
7
+ export const DEFAULT_AUTO_COMPACT_TOKENS = 88_000;
8
+ const KEEP_RECENT_MIN = 6;
9
+ const KEEP_RECENT_FLOOR = 2;
10
+ const HISTORY_BODY_CAP = 12_000;
11
+ const PART_OUTPUT_CAP = 2_000;
12
+ const PARTS_BLOCK_CAP = 8_000;
13
+ const MAX_TOOL_PARTS = 8;
14
+ const THINK_CAP = 400;
15
+ const SUMMARY_CAP = 4_000;
16
+
17
+ export {
18
+ SEND_TOKEN_BUDGET,
19
+ estimateSendTokens,
20
+ trimSendMessages,
21
+ } from "./send-budget.ts";
22
+
23
+ export type HistoryItem = {
24
+ id?: string;
25
+ author: string;
26
+ body: string;
27
+ parts?: ChatPart[];
28
+ };
29
+
30
+ export type CompactCheckpoint = {
31
+ throughId: string;
32
+ summary: string;
33
+ updatedAt: string;
34
+ messageCount: number;
35
+ };
36
+
37
+ export type PackedHistory = {
38
+ messages: { role: "user" | "assistant"; content: string }[];
39
+ compacted: boolean;
40
+ checkpoint: CompactCheckpoint | null;
41
+ };
42
+
43
+ export function estimateTokens(text: string): number {
44
+ return Math.ceil(String(text || "").length / CHARS_PER_TOKEN);
45
+ }
46
+
47
+ function clipText(text: string, cap: number): string {
48
+ const value = String(text || "");
49
+ if (value.length <= cap) return value;
50
+ return `${value.slice(0, cap)}\n… truncated …`;
51
+ }
52
+
53
+ export function clipHistoryItem(item: HistoryItem): HistoryItem {
54
+ const body = clipText(item.body, HISTORY_BODY_CAP);
55
+ if (!item.parts?.length) {
56
+ return body === item.body ? item : { ...item, body };
57
+ }
58
+ const tools = item.parts.filter(
59
+ (part) => part.type === "tool" || part.type === "skill",
60
+ );
61
+ const thinking = item.parts.find((part) => part.type === "thinking");
62
+ const textParts = item.parts.filter((part) => part.type === "text");
63
+ const clippedTools = tools.slice(-MAX_TOOL_PARTS).map((part) => {
64
+ if (part.type === "skill") {
65
+ return {
66
+ ...part,
67
+ output: part.output ? clipText(part.output, PART_OUTPUT_CAP) : part.output,
68
+ };
69
+ }
70
+ return { ...part, output: clipText(part.output, PART_OUTPUT_CAP) };
71
+ });
72
+ const parts: ChatPart[] = [];
73
+ if (thinking?.text) {
74
+ parts.push({
75
+ type: "thinking",
76
+ text: clipText(thinking.text, THINK_CAP),
77
+ });
78
+ }
79
+ parts.push(...clippedTools, ...textParts);
80
+ return { ...item, body, parts };
81
+ }
82
+
83
+ export function formatPartsForModel(parts: ChatPart[] | undefined): string {
84
+ if (!parts?.length) return "";
85
+ const lines: string[] = [];
86
+ for (const part of parts) {
87
+ if (part.type === "thinking") continue;
88
+ if (part.type === "text") continue;
89
+ if (part.type === "skill") {
90
+ lines.push(`skill ${part.name}`.trim());
91
+ continue;
92
+ }
93
+ const head = `${part.name} ${part.detail || ""}`.trim();
94
+ const out = String(part.output || "").trim();
95
+ lines.push(out ? `${head}\n${out}` : head);
96
+ }
97
+ const block = lines.join("\n\n");
98
+ if (block.length <= PARTS_BLOCK_CAP) return block;
99
+ return `${block.slice(0, PARTS_BLOCK_CAP)}\n… truncated …`;
100
+ }
101
+
102
+ export function toHistoryItem(message: {
103
+ id?: string;
104
+ author: string;
105
+ body: string;
106
+ parts?: ChatPart[];
107
+ }): HistoryItem {
108
+ return {
109
+ id: message.id,
110
+ author: message.author,
111
+ body: message.body,
112
+ ...(message.parts && message.parts.length ? { parts: message.parts } : {}),
113
+ };
114
+ }
115
+
116
+ export function toModelMessage(item: HistoryItem): {
117
+ role: "user" | "assistant";
118
+ content: string;
119
+ } {
120
+ const tools = formatPartsForModel(item.parts);
121
+ if (item.author === "you") {
122
+ return {
123
+ role: "user",
124
+ content: tools ? `${item.body}\n\n<tools>\n${tools}\n</tools>` : item.body,
125
+ };
126
+ }
127
+ const text = `${item.author}: ${item.body}`;
128
+ return {
129
+ role: "assistant",
130
+ content: tools ? `${text}\n\n<tools>\n${tools}\n</tools>` : text,
131
+ };
132
+ }
133
+
134
+ function messagesTokens(
135
+ messages: { role: string; content: string }[],
136
+ ): number {
137
+ return messages.reduce((sum, item) => sum + estimateTokens(item.content) + 8, 0);
138
+ }
139
+
140
+ export function localCompactSummary(items: HistoryItem[]): string {
141
+ const users = items
142
+ .filter((item) => item.author === "you")
143
+ .map((item) => String(item.body || "").replace(/\s+/g, " ").trim().slice(0, 160))
144
+ .filter(Boolean);
145
+ const bots = items.filter((item) => item.author !== "you");
146
+ const tools = [
147
+ ...new Set(
148
+ items.flatMap((item) =>
149
+ (item.parts || [])
150
+ .filter(
151
+ (part): part is Extract<ChatPart, { type: "tool" }> =>
152
+ part.type === "tool",
153
+ )
154
+ .map((part) => `${part.name} ${part.detail || ""}`.trim()),
155
+ ),
156
+ ),
157
+ ].slice(0, 24);
158
+ const lines = [
159
+ `${items.length} earlier messages compacted.`,
160
+ users.length ? `User asked: ${users.slice(0, 10).join(" | ")}` : "",
161
+ bots.length ? `Assistants replied in ${bots.length} turns.` : "",
162
+ tools.length ? `Tools: ${tools.join("; ")}` : "",
163
+ ].filter(Boolean);
164
+ return lines.join("\n").slice(0, SUMMARY_CAP);
165
+ }
166
+
167
+ export function compactPrefix(summary: string): {
168
+ role: "user" | "assistant";
169
+ content: string;
170
+ }[] {
171
+ const body = String(summary || "").trim() || "(empty compact)";
172
+ return [
173
+ {
174
+ role: "user",
175
+ content:
176
+ "# Conversation so far (compacted)\n" +
177
+ "Use this summary as prior context. Messages after it are the recent turns at full fidelity.\n\n" +
178
+ body,
179
+ },
180
+ {
181
+ role: "assistant",
182
+ content: "Understood. I'll continue from this summary plus the recent turns.",
183
+ },
184
+ ];
185
+ }
186
+
187
+ function lastId(items: HistoryItem[]): string {
188
+ return items[items.length - 1]?.id || `count:${items.length}`;
189
+ }
190
+
191
+ export function canReuseCheckpoint(
192
+ checkpoint: CompactCheckpoint | null | undefined,
193
+ old: HistoryItem[],
194
+ ): boolean {
195
+ if (!checkpoint || !old.length) return false;
196
+ if (!checkpoint.summary.trim()) return false;
197
+ if (checkpoint.messageCount !== old.length) return false;
198
+ return checkpoint.throughId === lastId(old);
199
+ }
200
+
201
+ export function planCompact(input: {
202
+ system: string;
203
+ history: HistoryItem[];
204
+ userMessage: string;
205
+ tokenLimit?: number;
206
+ }): { mode: "full" | "compact"; old: HistoryItem[]; recent: HistoryItem[] } {
207
+ const limit = input.tokenLimit ?? DEFAULT_AUTO_COMPACT_TOKENS;
208
+ const mapped = input.history.map(toModelMessage);
209
+ const user = { role: "user" as const, content: input.userMessage };
210
+ const fullCost =
211
+ estimateTokens(input.system) + messagesTokens([...mapped, user]);
212
+ if (fullCost <= limit) {
213
+ return { mode: "full", old: [], recent: input.history };
214
+ }
215
+ if (input.history.length <= 1) {
216
+ return { mode: "full", old: [], recent: input.history };
217
+ }
218
+
219
+ const prefixBudget =
220
+ estimateTokens(input.system) +
221
+ estimateTokens(compactPrefix("x").map((item) => item.content).join("\n")) +
222
+ 200;
223
+ const tailBudget = Math.max(512, limit - prefixBudget - estimateTokens(input.userMessage));
224
+ let recentCount = 0;
225
+ let used = 0;
226
+ for (let i = input.history.length - 1; i >= 0; i -= 1) {
227
+ const cost = estimateTokens(toModelMessage(input.history[i]).content) + 8;
228
+ if (recentCount >= KEEP_RECENT_FLOOR && used + cost > tailBudget) break;
229
+ if (recentCount >= KEEP_RECENT_MIN && used + cost > tailBudget) break;
230
+ used += cost;
231
+ recentCount += 1;
232
+ }
233
+ recentCount = Math.max(
234
+ Math.min(KEEP_RECENT_FLOOR, input.history.length),
235
+ recentCount,
236
+ );
237
+ const split = input.history.length - recentCount;
238
+ if (split <= 0) {
239
+ return { mode: "full", old: [], recent: input.history };
240
+ }
241
+ return {
242
+ mode: "compact",
243
+ old: input.history.slice(0, split),
244
+ recent: input.history.slice(split),
245
+ };
246
+ }
247
+
248
+ async function summarizeOld(input: {
249
+ old: HistoryItem[];
250
+ previous?: string;
251
+ dataDir: string;
252
+ env?: NodeJS.ProcessEnv;
253
+ prefer?: ModelRef | null;
254
+ }): Promise<string> {
255
+ const transcript = input.old
256
+ .map((item) => {
257
+ const who = item.author === "you" ? "User" : item.author;
258
+ const tools = (item.parts || [])
259
+ .filter(
260
+ (part): part is Extract<ChatPart, { type: "tool" }> =>
261
+ part.type === "tool",
262
+ )
263
+ .slice(-MAX_TOOL_PARTS)
264
+ .map((part) => `${part.name} ${part.detail || ""}`.trim())
265
+ .join("; ");
266
+ const line = `${who}: ${String(item.body || "").slice(0, 400)}`;
267
+ return tools ? `${line} [${tools}]` : line;
268
+ })
269
+ .join("\n")
270
+ .slice(0, 24_000);
271
+ const result = await llmComplete({
272
+ dataDir: input.dataDir,
273
+ env: input.env,
274
+ role: "compression",
275
+ prefer: input.prefer,
276
+ tools: false,
277
+ temperature: 0.1,
278
+ system:
279
+ "You compact a conversation so work can continue. Output only the summary.",
280
+ messages: [
281
+ {
282
+ role: "user",
283
+ content:
284
+ "Summarize the older conversation for continuing work. Capture goals, decisions, constraints, files/tools used, and open questions. Do not mention this summarization. Be dense.\n\n" +
285
+ (input.previous?.trim()
286
+ ? `Previous compact:\n${input.previous.trim()}\n\n`
287
+ : "") +
288
+ `Older messages:\n${transcript}`,
289
+ },
290
+ ],
291
+ });
292
+ const text = result?.text?.trim() || "";
293
+ if (text.length >= 24 && !/模型請求|unauthorized|login failed/i.test(text)) {
294
+ return text.slice(0, SUMMARY_CAP);
295
+ }
296
+ return localCompactSummary(input.old);
297
+ }
298
+
299
+ export async function packHistory(input: {
300
+ system: string;
301
+ history: HistoryItem[];
302
+ userMessage: string;
303
+ dataDir: string;
304
+ env?: NodeJS.ProcessEnv;
305
+ prefer?: ModelRef | null;
306
+ checkpoint?: CompactCheckpoint | null;
307
+ tokenLimit?: number;
308
+ }): Promise<PackedHistory> {
309
+ const user = { role: "user" as const, content: input.userMessage };
310
+ const history = input.history.map(clipHistoryItem);
311
+ const plan = planCompact({
312
+ system: input.system,
313
+ history,
314
+ userMessage: input.userMessage,
315
+ tokenLimit: input.tokenLimit,
316
+ });
317
+ if (plan.mode === "full") {
318
+ return {
319
+ messages: [...plan.recent.map(toModelMessage), user],
320
+ compacted: false,
321
+ checkpoint: input.checkpoint ?? null,
322
+ };
323
+ }
324
+
325
+ let summary = "";
326
+ let checkpoint: CompactCheckpoint;
327
+ if (canReuseCheckpoint(input.checkpoint, plan.old)) {
328
+ summary = input.checkpoint!.summary;
329
+ checkpoint = input.checkpoint!;
330
+ } else {
331
+ summary = await summarizeOld({
332
+ old: plan.old,
333
+ previous: input.checkpoint?.summary,
334
+ dataDir: input.dataDir,
335
+ env: input.env,
336
+ prefer: input.prefer,
337
+ });
338
+ checkpoint = {
339
+ throughId: lastId(plan.old),
340
+ summary,
341
+ updatedAt: new Date().toISOString(),
342
+ messageCount: plan.old.length,
343
+ };
344
+ }
345
+
346
+ return {
347
+ messages: [
348
+ ...compactPrefix(summary),
349
+ ...plan.recent.map(toModelMessage),
350
+ user,
351
+ ],
352
+ compacted: true,
353
+ checkpoint,
354
+ };
355
+ }
@@ -0,0 +1,40 @@
1
+ import type { StoreService } from "./plugins/store.ts";
2
+ import type { HarnessService } from "./plugins/harness.ts";
3
+ import type { OAuthService } from "./plugins/oauth.ts";
4
+ import type { LlmService } from "./plugins/llm.ts";
5
+ import type { ToolsService } from "./plugins/tools.ts";
6
+ import type { McpService } from "./plugins/mcp.ts";
7
+ import type { ChatService } from "./plugins/chat.ts";
8
+ import type { MemoryService } from "./plugins/memory.ts";
9
+ import type { ServerService } from "./plugins/server.ts";
10
+
11
+ declare module "cordis" {
12
+ interface Context {
13
+ guildEnv: NodeJS.ProcessEnv;
14
+ store: StoreService;
15
+ harness: HarnessService;
16
+ oauth: OAuthService;
17
+ llm: LlmService;
18
+ tools: ToolsService;
19
+ mcp: McpService;
20
+ chat: ChatService;
21
+ memory: MemoryService;
22
+ server: ServerService;
23
+ }
24
+
25
+ interface Events {
26
+ "guild/listening"(info: {
27
+ host: string;
28
+ port: number;
29
+ dataDir: string;
30
+ }): void;
31
+ "guild/turn-complete"(turn: {
32
+ roomId: string;
33
+ botId: string;
34
+ userText: string;
35
+ reply: string;
36
+ }): void;
37
+ }
38
+ }
39
+
40
+ export {};