stagent 0.1.10 → 0.1.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.
- package/README.md +58 -27
- package/package.json +3 -3
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -21
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/costs/page.tsx +53 -43
- package/src/app/globals.css +0 -5
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/page.tsx +5 -0
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/notification-item.tsx +6 -3
- package/src/components/notifications/pending-approval-host.tsx +57 -11
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +223 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-detail-view.tsx +7 -19
- package/src/components/profiles/profile-form-view.tsx +0 -22
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/workflows/loop-status-view.tsx +8 -4
- package/src/components/workflows/workflow-status-view.tsx +16 -9
- package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
- package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
- package/src/lib/agents/__tests__/sweep.test.ts +202 -0
- package/src/lib/agents/claude-agent.ts +104 -78
- package/src/lib/agents/learned-context.ts +32 -28
- package/src/lib/agents/learning-session.ts +234 -0
- package/src/lib/agents/pattern-extractor.ts +34 -64
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/registry.ts +0 -1
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/agents/profiles/types.ts +0 -1
- package/src/lib/agents/runtime/catalog.ts +1 -1
- package/src/lib/agents/runtime/claude.ts +66 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +6 -0
- package/src/lib/data/seed-data/profiles.ts +0 -3
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +102 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +29 -5
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +4 -2
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -41
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/profile.test.ts +0 -15
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/profile.ts +0 -1
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/__tests__/engine.test.ts +2 -0
- package/src/lib/workflows/engine.ts +20 -1
|
@@ -2,7 +2,7 @@ import { db } from "@/lib/db";
|
|
|
2
2
|
import { learnedContext, notifications } from "@/lib/db/schema";
|
|
3
3
|
import { and, desc, eq } from "drizzle-orm";
|
|
4
4
|
import type { LearnedContextRow } from "@/lib/db/schema";
|
|
5
|
-
import
|
|
5
|
+
import { runMetaCompletion } from "./runtime/claude";
|
|
6
6
|
|
|
7
7
|
const CONTEXT_CHAR_LIMIT = 8_000;
|
|
8
8
|
const SUMMARIZATION_THRESHOLD = 6_000;
|
|
@@ -58,14 +58,24 @@ function getNextVersion(profileId: string): number {
|
|
|
58
58
|
// Proposal flow
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Insert a context proposal and optionally create a notification for human review.
|
|
63
|
+
*
|
|
64
|
+
* When `options.silent` is true, the proposal row is created but no notification
|
|
65
|
+
* is generated. This is used by the learning session system to buffer proposals
|
|
66
|
+
* during workflow execution — a batch notification is created when the session closes.
|
|
67
|
+
*
|
|
68
|
+
* Returns the learned_context row ID (not the notification ID) so callers can
|
|
69
|
+
* reference the proposal regardless of whether a notification was created.
|
|
70
|
+
*/
|
|
62
71
|
export async function proposeContextAddition(
|
|
63
72
|
profileId: string,
|
|
64
73
|
taskId: string,
|
|
65
|
-
additions: string
|
|
74
|
+
additions: string,
|
|
75
|
+
options?: { silent?: boolean }
|
|
66
76
|
): Promise<string> {
|
|
67
77
|
const version = getNextVersion(profileId);
|
|
68
|
-
const notificationId = crypto.randomUUID();
|
|
78
|
+
const notificationId = options?.silent ? null : crypto.randomUUID();
|
|
69
79
|
const rowId = crypto.randomUUID();
|
|
70
80
|
const now = new Date();
|
|
71
81
|
|
|
@@ -83,19 +93,21 @@ export async function proposeContextAddition(
|
|
|
83
93
|
createdAt: now,
|
|
84
94
|
});
|
|
85
95
|
|
|
86
|
-
// Create notification for human review
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
// Create notification for human review (unless silent)
|
|
97
|
+
if (notificationId) {
|
|
98
|
+
await db.insert(notifications).values({
|
|
99
|
+
id: notificationId,
|
|
100
|
+
taskId,
|
|
101
|
+
type: "context_proposal",
|
|
102
|
+
title: `Context proposal for ${profileId}`,
|
|
103
|
+
body: additions.slice(0, 500),
|
|
104
|
+
toolName: profileId,
|
|
105
|
+
toolInput: JSON.stringify({ profileId, additions, learnedContextId: rowId }),
|
|
106
|
+
createdAt: now,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
97
109
|
|
|
98
|
-
return
|
|
110
|
+
return rowId;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
// ---------------------------------------------------------------------------
|
|
@@ -243,14 +255,8 @@ export async function summarizeContext(profileId: string): Promise<void> {
|
|
|
243
255
|
const content = getActiveLearnedContext(profileId);
|
|
244
256
|
if (!content || content.length <= SUMMARIZATION_THRESHOLD) return;
|
|
245
257
|
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
model: "claude-sonnet-4-20250514",
|
|
249
|
-
max_tokens: 2048,
|
|
250
|
-
messages: [
|
|
251
|
-
{
|
|
252
|
-
role: "user",
|
|
253
|
-
content: `You are condensing learned context for an AI agent profile "${profileId}".
|
|
258
|
+
const { text } = await runMetaCompletion({
|
|
259
|
+
prompt: `You are condensing learned context for an AI agent profile "${profileId}".
|
|
254
260
|
The current context has grown to ${content.length} characters and needs to be summarized to under ${SUMMARIZATION_THRESHOLD} characters while preserving all key patterns, best practices, and important insights.
|
|
255
261
|
|
|
256
262
|
Current learned context:
|
|
@@ -266,12 +272,10 @@ Produce a condensed version that:
|
|
|
266
272
|
5. Stays under ${SUMMARIZATION_THRESHOLD} characters
|
|
267
273
|
|
|
268
274
|
Output ONLY the condensed context, no preamble.`,
|
|
269
|
-
|
|
270
|
-
],
|
|
275
|
+
activityType: "context_summarization",
|
|
271
276
|
});
|
|
272
277
|
|
|
273
|
-
const summarized =
|
|
274
|
-
response.content[0].type === "text" ? response.content[0].text : "";
|
|
278
|
+
const summarized = text.trim();
|
|
275
279
|
|
|
276
280
|
if (!summarized || summarized.length >= content.length) return;
|
|
277
281
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning Session — buffers context proposals during workflow execution.
|
|
3
|
+
*
|
|
4
|
+
* When a workflow starts, a session is opened. All context proposals generated
|
|
5
|
+
* by tasks within that workflow are buffered instead of creating individual
|
|
6
|
+
* notifications. When the workflow completes (or fails), the session is closed
|
|
7
|
+
* and a single batch notification is created for all buffered proposals.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { db } from "@/lib/db";
|
|
11
|
+
import { learnedContext, notifications, tasks } from "@/lib/db/schema";
|
|
12
|
+
import { eq, and } from "drizzle-orm";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// In-memory session registry
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Active learning sessions, keyed by workflowId.
|
|
20
|
+
* Each session accumulates proposal IDs (learnedContext row IDs) until closed.
|
|
21
|
+
*/
|
|
22
|
+
const activeSessions = new Map<
|
|
23
|
+
string,
|
|
24
|
+
{
|
|
25
|
+
workflowId: string;
|
|
26
|
+
proposalIds: string[];
|
|
27
|
+
openedAt: Date;
|
|
28
|
+
}
|
|
29
|
+
>();
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Session lifecycle
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Open a learning session for a workflow. All proposals created during this
|
|
37
|
+
* session will be buffered instead of generating individual notifications.
|
|
38
|
+
*/
|
|
39
|
+
export function openLearningSession(workflowId: string): void {
|
|
40
|
+
activeSessions.set(workflowId, {
|
|
41
|
+
workflowId,
|
|
42
|
+
proposalIds: [],
|
|
43
|
+
openedAt: new Date(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a learning session is active for a workflow.
|
|
49
|
+
*/
|
|
50
|
+
export function hasLearningSession(workflowId: string): boolean {
|
|
51
|
+
return activeSessions.has(workflowId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the active session for a workflow (or null if none).
|
|
56
|
+
*/
|
|
57
|
+
export function getLearningSession(workflowId: string) {
|
|
58
|
+
return activeSessions.get(workflowId) ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Buffer a proposal ID into the active session.
|
|
63
|
+
* Called by proposeContextAddition when it detects a workflow session.
|
|
64
|
+
*/
|
|
65
|
+
export function bufferProposal(
|
|
66
|
+
workflowId: string,
|
|
67
|
+
proposalRowId: string
|
|
68
|
+
): void {
|
|
69
|
+
const session = activeSessions.get(workflowId);
|
|
70
|
+
if (session) {
|
|
71
|
+
session.proposalIds.push(proposalRowId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Close the learning session and create a single batch notification
|
|
77
|
+
* for all buffered proposals. Returns the batch notification ID,
|
|
78
|
+
* or null if there were no proposals.
|
|
79
|
+
*/
|
|
80
|
+
export async function closeLearningSession(
|
|
81
|
+
workflowId: string
|
|
82
|
+
): Promise<string | null> {
|
|
83
|
+
const session = activeSessions.get(workflowId);
|
|
84
|
+
activeSessions.delete(workflowId);
|
|
85
|
+
|
|
86
|
+
if (!session || session.proposalIds.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Load all buffered proposal rows
|
|
91
|
+
const proposals = [];
|
|
92
|
+
for (const id of session.proposalIds) {
|
|
93
|
+
const [row] = db
|
|
94
|
+
.select()
|
|
95
|
+
.from(learnedContext)
|
|
96
|
+
.where(eq(learnedContext.id, id))
|
|
97
|
+
.all();
|
|
98
|
+
if (row) {
|
|
99
|
+
proposals.push(row);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (proposals.length === 0) return null;
|
|
104
|
+
|
|
105
|
+
// Group by profile for the notification body
|
|
106
|
+
const byProfile = new Map<string, typeof proposals>();
|
|
107
|
+
for (const p of proposals) {
|
|
108
|
+
const group = byProfile.get(p.profileId) ?? [];
|
|
109
|
+
group.push(p);
|
|
110
|
+
byProfile.set(p.profileId, group);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Build summary body
|
|
114
|
+
const bodyLines: string[] = [];
|
|
115
|
+
for (const [profileId, group] of byProfile) {
|
|
116
|
+
bodyLines.push(`**${profileId}** (${group.length} proposal${group.length > 1 ? "s" : ""}):`);
|
|
117
|
+
for (const p of group) {
|
|
118
|
+
const preview = (p.proposedAdditions ?? p.diff ?? "").slice(0, 150);
|
|
119
|
+
bodyLines.push(` - ${preview}${preview.length >= 150 ? "..." : ""}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const notificationId = crypto.randomUUID();
|
|
124
|
+
|
|
125
|
+
await db.insert(notifications).values({
|
|
126
|
+
id: notificationId,
|
|
127
|
+
taskId: null,
|
|
128
|
+
type: "context_proposal_batch",
|
|
129
|
+
title: `Workflow learning: ${proposals.length} context proposal${proposals.length > 1 ? "s" : ""}`,
|
|
130
|
+
body: bodyLines.join("\n").slice(0, 1000),
|
|
131
|
+
toolName: "workflow-context-batch",
|
|
132
|
+
toolInput: JSON.stringify({
|
|
133
|
+
workflowId,
|
|
134
|
+
proposalIds: session.proposalIds,
|
|
135
|
+
profileIds: [...byProfile.keys()],
|
|
136
|
+
}),
|
|
137
|
+
createdAt: new Date(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return notificationId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Task → Workflow mapping
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve the workflowId for a given task. Returns null if the task is
|
|
149
|
+
* not part of a workflow or the task doesn't exist.
|
|
150
|
+
*/
|
|
151
|
+
export function getTaskWorkflowId(taskId: string): string | null {
|
|
152
|
+
try {
|
|
153
|
+
const [row] = db
|
|
154
|
+
.select({ workflowId: tasks.workflowId })
|
|
155
|
+
.from(tasks)
|
|
156
|
+
.where(eq(tasks.id, taskId))
|
|
157
|
+
.all();
|
|
158
|
+
|
|
159
|
+
return row?.workflowId ?? null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Batch operations
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Approve all proposals in a batch by their IDs.
|
|
171
|
+
*/
|
|
172
|
+
export async function batchApproveProposals(
|
|
173
|
+
proposalRowIds: string[]
|
|
174
|
+
): Promise<number> {
|
|
175
|
+
// Import inline to avoid circular dependency
|
|
176
|
+
const { approveProposal } = await import("./learned-context");
|
|
177
|
+
|
|
178
|
+
let approved = 0;
|
|
179
|
+
for (const rowId of proposalRowIds) {
|
|
180
|
+
const [row] = db
|
|
181
|
+
.select()
|
|
182
|
+
.from(learnedContext)
|
|
183
|
+
.where(
|
|
184
|
+
and(
|
|
185
|
+
eq(learnedContext.id, rowId),
|
|
186
|
+
eq(learnedContext.changeType, "proposal")
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
.all();
|
|
190
|
+
|
|
191
|
+
if (row?.proposalNotificationId) {
|
|
192
|
+
try {
|
|
193
|
+
await approveProposal(row.proposalNotificationId);
|
|
194
|
+
approved++;
|
|
195
|
+
} catch {
|
|
196
|
+
// Skip if already approved/rejected
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return approved;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reject all proposals in a batch by their IDs.
|
|
205
|
+
*/
|
|
206
|
+
export async function batchRejectProposals(
|
|
207
|
+
proposalRowIds: string[]
|
|
208
|
+
): Promise<number> {
|
|
209
|
+
const { rejectProposal } = await import("./learned-context");
|
|
210
|
+
|
|
211
|
+
let rejected = 0;
|
|
212
|
+
for (const rowId of proposalRowIds) {
|
|
213
|
+
const [row] = db
|
|
214
|
+
.select()
|
|
215
|
+
.from(learnedContext)
|
|
216
|
+
.where(
|
|
217
|
+
and(
|
|
218
|
+
eq(learnedContext.id, rowId),
|
|
219
|
+
eq(learnedContext.changeType, "proposal")
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
.all();
|
|
223
|
+
|
|
224
|
+
if (row?.proposalNotificationId) {
|
|
225
|
+
try {
|
|
226
|
+
await rejectProposal(row.proposalNotificationId);
|
|
227
|
+
rejected++;
|
|
228
|
+
} catch {
|
|
229
|
+
// Skip if already approved/rejected
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return rejected;
|
|
234
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
2
1
|
import { db } from "@/lib/db";
|
|
3
2
|
import { tasks, agentLogs } from "@/lib/db/schema";
|
|
4
3
|
import { eq, desc } from "drizzle-orm";
|
|
@@ -6,6 +5,12 @@ import {
|
|
|
6
5
|
getActiveLearnedContext,
|
|
7
6
|
proposeContextAddition,
|
|
8
7
|
} from "./learned-context";
|
|
8
|
+
import { runMetaCompletion } from "./runtime/claude";
|
|
9
|
+
import {
|
|
10
|
+
getTaskWorkflowId,
|
|
11
|
+
hasLearningSession,
|
|
12
|
+
bufferProposal,
|
|
13
|
+
} from "./learning-session";
|
|
9
14
|
|
|
10
15
|
export interface PatternEntry {
|
|
11
16
|
title: string;
|
|
@@ -17,50 +22,9 @@ export interface PatternProposal {
|
|
|
17
22
|
patterns: PatternEntry[];
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
const PATTERN_TOOL: Anthropic.Messages.Tool = {
|
|
21
|
-
name: "propose_learned_patterns",
|
|
22
|
-
description:
|
|
23
|
-
"Propose patterns learned from this task execution that should be remembered for future tasks with this profile.",
|
|
24
|
-
input_schema: {
|
|
25
|
-
type: "object" as const,
|
|
26
|
-
properties: {
|
|
27
|
-
patterns: {
|
|
28
|
-
type: "array",
|
|
29
|
-
items: {
|
|
30
|
-
type: "object",
|
|
31
|
-
properties: {
|
|
32
|
-
title: {
|
|
33
|
-
type: "string",
|
|
34
|
-
description: "Short pattern name (2-6 words)",
|
|
35
|
-
},
|
|
36
|
-
description: {
|
|
37
|
-
type: "string",
|
|
38
|
-
description:
|
|
39
|
-
"Concise description of the pattern or lesson (1-2 sentences)",
|
|
40
|
-
},
|
|
41
|
-
category: {
|
|
42
|
-
type: "string",
|
|
43
|
-
enum: [
|
|
44
|
-
"error_resolution",
|
|
45
|
-
"best_practice",
|
|
46
|
-
"shortcut",
|
|
47
|
-
"preference",
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
required: ["title", "description", "category"],
|
|
52
|
-
},
|
|
53
|
-
description:
|
|
54
|
-
"Patterns worth remembering. Return empty array if nothing notable.",
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
required: ["patterns"],
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
25
|
/**
|
|
62
26
|
* Analyze a completed task for patterns worth learning.
|
|
63
|
-
*
|
|
27
|
+
* Routes through the Claude Agent SDK runtime (no direct Anthropic SDK usage).
|
|
64
28
|
* Returns the notification ID if a proposal was created, null otherwise.
|
|
65
29
|
*/
|
|
66
30
|
export async function analyzeForLearnedPatterns(
|
|
@@ -99,16 +63,13 @@ export async function analyzeForLearnedPatterns(
|
|
|
99
63
|
})
|
|
100
64
|
.join("\n");
|
|
101
65
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
role: "user",
|
|
111
|
-
content: `Analyze this completed task for patterns worth learning for the "${profileId}" agent profile.
|
|
66
|
+
const { text } = await runMetaCompletion({
|
|
67
|
+
prompt: `Analyze this completed task for patterns worth learning for the "${profileId}" agent profile.
|
|
68
|
+
|
|
69
|
+
Return ONLY a JSON array (no markdown, no code fences):
|
|
70
|
+
[{"title": "...", "description": "...", "category": "error_resolution|best_practice|shortcut|preference"}]
|
|
71
|
+
|
|
72
|
+
Return an empty array [] if no noteworthy patterns.
|
|
112
73
|
|
|
113
74
|
## Task
|
|
114
75
|
Title: ${task.title}
|
|
@@ -124,27 +85,36 @@ ${logSummary.slice(0, 2000)}
|
|
|
124
85
|
${currentContext ?? "(none yet)"}
|
|
125
86
|
|
|
126
87
|
Extract ONLY genuinely useful patterns — things that would help this profile avoid mistakes or work more efficiently on similar future tasks. If this task was routine with nothing notable, return an empty patterns array. Do NOT repeat patterns already in the learned context.`,
|
|
127
|
-
|
|
128
|
-
],
|
|
88
|
+
activityType: "pattern_extraction",
|
|
129
89
|
});
|
|
130
90
|
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
if (!toolBlock || toolBlock.type !== "tool_use") return null;
|
|
91
|
+
// Parse JSON array from response text
|
|
92
|
+
const jsonMatch = text.match(/\[[\s\S]*\]/);
|
|
93
|
+
const patterns: PatternEntry[] = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
|
|
137
94
|
|
|
138
|
-
|
|
139
|
-
if (!proposal.patterns || proposal.patterns.length === 0) return null;
|
|
95
|
+
if (patterns.length === 0) return null;
|
|
140
96
|
|
|
141
97
|
// Format patterns as text for the proposal
|
|
142
|
-
const formattedAdditions =
|
|
98
|
+
const formattedAdditions = patterns
|
|
143
99
|
.map(
|
|
144
100
|
(p) =>
|
|
145
101
|
`### ${p.title} [${p.category}]\n${p.description}`
|
|
146
102
|
)
|
|
147
103
|
.join("\n\n");
|
|
148
104
|
|
|
105
|
+
// Check if this task is part of a workflow with an active learning session.
|
|
106
|
+
// If so, buffer the proposal instead of creating an individual notification.
|
|
107
|
+
const workflowId = getTaskWorkflowId(taskId);
|
|
108
|
+
if (workflowId && hasLearningSession(workflowId)) {
|
|
109
|
+
const rowId = await proposeContextAddition(
|
|
110
|
+
profileId,
|
|
111
|
+
taskId,
|
|
112
|
+
formattedAdditions,
|
|
113
|
+
{ silent: true }
|
|
114
|
+
);
|
|
115
|
+
bufferProposal(workflowId, rowId);
|
|
116
|
+
return rowId;
|
|
117
|
+
}
|
|
118
|
+
|
|
149
119
|
return proposeContextAddition(profileId, taskId, formattedAdditions);
|
|
150
120
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentProfile } from "../types";
|
|
2
|
+
|
|
3
|
+
import { sortProfilesByName } from "../sort";
|
|
4
|
+
|
|
5
|
+
function makeProfile(id: string, name: string): AgentProfile {
|
|
6
|
+
return {
|
|
7
|
+
id,
|
|
8
|
+
name,
|
|
9
|
+
description: `${name} description`,
|
|
10
|
+
domain: "work",
|
|
11
|
+
tags: [],
|
|
12
|
+
systemPrompt: "",
|
|
13
|
+
skillMd: "",
|
|
14
|
+
allowedTools: [],
|
|
15
|
+
mcpServers: {},
|
|
16
|
+
canUseToolPolicy: false,
|
|
17
|
+
maxTurns: 20,
|
|
18
|
+
outputFormat: "text",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
author: "test",
|
|
21
|
+
source: "test",
|
|
22
|
+
tests: [],
|
|
23
|
+
supportedRuntimes: ["claude-code"],
|
|
24
|
+
runtimeOverrides: {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("sortProfilesByName", () => {
|
|
29
|
+
it("sorts profiles alphabetically so sweep remains discoverable", () => {
|
|
30
|
+
const sorted = sortProfilesByName([
|
|
31
|
+
makeProfile("sweep", "Sweep"),
|
|
32
|
+
makeProfile("general", "General"),
|
|
33
|
+
makeProfile("api-tester", "API Tester"),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
expect(sorted.map((profile) => profile.id)).toEqual([
|
|
37
|
+
"api-tester",
|
|
38
|
+
"general",
|
|
39
|
+
"sweep",
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -12,7 +12,6 @@ runtimeOverrides:
|
|
|
12
12
|
Stay pragmatic, execute the requested work directly, and prefer concise operational updates.
|
|
13
13
|
Keep outputs grounded in the current workspace and call out blocked actions explicitly.
|
|
14
14
|
|
|
15
|
-
temperature: 0.5
|
|
16
15
|
maxTurns: 30
|
|
17
16
|
|
|
18
17
|
author: stagent
|
|
@@ -174,7 +174,6 @@ function scanProfiles(): Map<string, AgentProfile> {
|
|
|
174
174
|
allowedTools: config.allowedTools,
|
|
175
175
|
mcpServers: config.mcpServers as Record<string, unknown>,
|
|
176
176
|
canUseToolPolicy: config.canUseToolPolicy,
|
|
177
|
-
temperature: config.temperature,
|
|
178
177
|
maxTurns: config.maxTurns,
|
|
179
178
|
outputFormat: config.outputFormat,
|
|
180
179
|
version: config.version,
|
|
@@ -50,7 +50,7 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
|
|
|
50
50
|
resume: true,
|
|
51
51
|
cancel: true,
|
|
52
52
|
approvals: true,
|
|
53
|
-
mcpServers:
|
|
53
|
+
mcpServers: false, // Not yet wired — configs not passed to codex subprocess
|
|
54
54
|
profileTests: false,
|
|
55
55
|
taskAssist: true,
|
|
56
56
|
authHealthCheck: true,
|