stagent 0.1.11 → 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.
Files changed (81) hide show
  1. package/README.md +35 -4
  2. package/package.json +3 -2
  3. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  4. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  5. package/src/__tests__/e2e/helpers.ts +286 -0
  6. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  7. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  8. package/src/__tests__/e2e/setup.ts +156 -0
  9. package/src/__tests__/e2e/single-task.test.ts +170 -0
  10. package/src/app/api/command-palette/recent/route.ts +41 -18
  11. package/src/app/api/context/batch/route.ts +44 -0
  12. package/src/app/api/permissions/presets/route.ts +80 -0
  13. package/src/app/api/playbook/status/route.ts +15 -0
  14. package/src/app/api/profiles/route.ts +23 -20
  15. package/src/app/api/settings/pricing/route.ts +15 -0
  16. package/src/app/costs/page.tsx +53 -43
  17. package/src/app/playbook/[slug]/page.tsx +76 -0
  18. package/src/app/playbook/page.tsx +54 -0
  19. package/src/app/profiles/page.tsx +7 -4
  20. package/src/app/settings/page.tsx +2 -2
  21. package/src/components/costs/cost-dashboard.tsx +226 -320
  22. package/src/components/dashboard/activity-feed.tsx +6 -2
  23. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  24. package/src/components/notifications/notification-item.tsx +6 -3
  25. package/src/components/notifications/pending-approval-host.tsx +57 -11
  26. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  27. package/src/components/playbook/journey-card.tsx +110 -0
  28. package/src/components/playbook/playbook-action-button.tsx +22 -0
  29. package/src/components/playbook/playbook-browser.tsx +143 -0
  30. package/src/components/playbook/playbook-card.tsx +102 -0
  31. package/src/components/playbook/playbook-detail-view.tsx +223 -0
  32. package/src/components/playbook/playbook-homepage.tsx +142 -0
  33. package/src/components/playbook/playbook-toc.tsx +90 -0
  34. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  35. package/src/components/playbook/related-docs.tsx +30 -0
  36. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  37. package/src/components/profiles/context-proposal-review.tsx +7 -3
  38. package/src/components/profiles/learned-context-panel.tsx +116 -8
  39. package/src/components/profiles/profile-detail-view.tsx +6 -3
  40. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  41. package/src/components/settings/api-key-form.tsx +5 -43
  42. package/src/components/settings/auth-config-section.tsx +10 -6
  43. package/src/components/settings/auth-status-badge.tsx +8 -0
  44. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  45. package/src/components/settings/connection-test-control.tsx +63 -0
  46. package/src/components/settings/permissions-section.tsx +85 -75
  47. package/src/components/settings/permissions-sections.tsx +24 -0
  48. package/src/components/settings/presets-section.tsx +159 -0
  49. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  50. package/src/components/shared/app-sidebar.tsx +2 -0
  51. package/src/components/shared/command-palette.tsx +30 -0
  52. package/src/components/shared/light-markdown.tsx +134 -0
  53. package/src/components/workflows/loop-status-view.tsx +8 -4
  54. package/src/components/workflows/workflow-status-view.tsx +16 -9
  55. package/src/lib/agents/learned-context.ts +27 -15
  56. package/src/lib/agents/learning-session.ts +234 -0
  57. package/src/lib/agents/pattern-extractor.ts +19 -0
  58. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  59. package/src/lib/agents/profiles/sort.ts +7 -0
  60. package/src/lib/constants/settings.ts +1 -0
  61. package/src/lib/db/schema.ts +3 -0
  62. package/src/lib/docs/adoption.ts +105 -0
  63. package/src/lib/docs/journey-tracker.ts +21 -0
  64. package/src/lib/docs/reader.ts +102 -0
  65. package/src/lib/docs/types.ts +54 -0
  66. package/src/lib/docs/usage-stage.ts +60 -0
  67. package/src/lib/notifications/actionable.ts +18 -10
  68. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  69. package/src/lib/settings/budget-guardrails.ts +213 -85
  70. package/src/lib/settings/permission-presets.ts +150 -0
  71. package/src/lib/settings/runtime-setup.ts +71 -0
  72. package/src/lib/usage/__tests__/ledger.test.ts +2 -2
  73. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  74. package/src/lib/usage/ledger.ts +1 -1
  75. package/src/lib/usage/pricing-registry.ts +570 -0
  76. package/src/lib/usage/pricing.ts +15 -95
  77. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  78. package/src/lib/utils/learned-context-history.ts +150 -0
  79. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  80. package/src/lib/validators/settings.ts +3 -9
  81. package/src/lib/workflows/engine.ts +18 -0
@@ -0,0 +1,171 @@
1
+ import type { LearnedContextRow } from "@/lib/db/schema";
2
+
3
+ import {
4
+ buildLearnedContextHistoryEntries,
5
+ buildUnifiedDiff,
6
+ hasMeaningfulDerivedDiff,
7
+ } from "../learned-context-history";
8
+
9
+ function makeRow(
10
+ overrides: Partial<LearnedContextRow> & {
11
+ id: string;
12
+ version: number;
13
+ changeType: LearnedContextRow["changeType"];
14
+ }
15
+ ): LearnedContextRow {
16
+ return {
17
+ id: overrides.id,
18
+ profileId: overrides.profileId ?? "general",
19
+ version: overrides.version,
20
+ content: overrides.content ?? null,
21
+ diff: overrides.diff ?? null,
22
+ changeType: overrides.changeType,
23
+ sourceTaskId: overrides.sourceTaskId ?? null,
24
+ proposalNotificationId: overrides.proposalNotificationId ?? null,
25
+ proposedAdditions: overrides.proposedAdditions ?? null,
26
+ approvedBy: overrides.approvedBy ?? null,
27
+ createdAt: overrides.createdAt ?? new Date("2026-03-17T12:00:00.000Z"),
28
+ };
29
+ }
30
+
31
+ describe("buildUnifiedDiff", () => {
32
+ it("treats the first version as all additions", () => {
33
+ expect(buildUnifiedDiff(null, "Alpha\nBeta")).toEqual([
34
+ { kind: "added", value: "Alpha" },
35
+ { kind: "added", value: "Beta" },
36
+ ]);
37
+ });
38
+
39
+ it("builds a unified diff for appended content", () => {
40
+ expect(buildUnifiedDiff("Alpha", "Alpha\nBeta")).toEqual([
41
+ { kind: "context", value: "Alpha" },
42
+ { kind: "added", value: "Beta" },
43
+ ]);
44
+ });
45
+ });
46
+
47
+ describe("buildLearnedContextHistoryEntries", () => {
48
+ it("derives rollback diffs from the previous snapshot version", () => {
49
+ const history = [
50
+ makeRow({
51
+ id: "v3",
52
+ version: 3,
53
+ changeType: "rollback",
54
+ content: "Alpha",
55
+ diff: "Rolled back to version 1",
56
+ }),
57
+ makeRow({
58
+ id: "v2",
59
+ version: 2,
60
+ changeType: "approved",
61
+ content: "Alpha\nBeta",
62
+ diff: "Beta",
63
+ }),
64
+ makeRow({
65
+ id: "v1",
66
+ version: 1,
67
+ changeType: "approved",
68
+ content: "Alpha",
69
+ diff: "Alpha",
70
+ }),
71
+ ];
72
+
73
+ const entries = buildLearnedContextHistoryEntries(history);
74
+ const rollbackEntry = entries[0];
75
+
76
+ expect(rollbackEntry.snapshotContent).toBe("Alpha");
77
+ expect(rollbackEntry.derivedDiff).toEqual({
78
+ previousVersion: 2,
79
+ lines: [
80
+ { kind: "context", value: "Alpha" },
81
+ { kind: "removed", value: "Beta" },
82
+ ],
83
+ });
84
+ });
85
+
86
+ it("derives summarization diffs from the previous snapshot version", () => {
87
+ const history = [
88
+ makeRow({
89
+ id: "v3",
90
+ version: 3,
91
+ changeType: "summarization",
92
+ content: "Alpha\nGamma",
93
+ diff: "Summarized from 14 to 11 chars",
94
+ }),
95
+ makeRow({
96
+ id: "v2",
97
+ version: 2,
98
+ changeType: "rejected",
99
+ content: "Alpha\nBeta",
100
+ diff: "Rejected addition",
101
+ }),
102
+ makeRow({
103
+ id: "v1",
104
+ version: 1,
105
+ changeType: "approved",
106
+ content: "Alpha\nBeta",
107
+ diff: "Alpha\nBeta",
108
+ }),
109
+ ];
110
+
111
+ const entries = buildLearnedContextHistoryEntries(history);
112
+ const summarizationEntry = entries[0];
113
+
114
+ expect(summarizationEntry.derivedDiff).toEqual({
115
+ previousVersion: 1,
116
+ lines: [
117
+ { kind: "context", value: "Alpha" },
118
+ { kind: "removed", value: "Beta" },
119
+ { kind: "added", value: "Gamma" },
120
+ ],
121
+ });
122
+ });
123
+
124
+ it("does not create derived diffs for proposal-only rows", () => {
125
+ const history = [
126
+ makeRow({
127
+ id: "proposal",
128
+ version: 2,
129
+ changeType: "proposal",
130
+ diff: "Add retries",
131
+ }),
132
+ makeRow({
133
+ id: "approved",
134
+ version: 1,
135
+ changeType: "approved",
136
+ content: "Validate inputs",
137
+ diff: "Validate inputs",
138
+ }),
139
+ ];
140
+
141
+ const entries = buildLearnedContextHistoryEntries(history);
142
+
143
+ expect(entries[0].snapshotContent).toBeNull();
144
+ expect(entries[0].derivedDiff).toBeNull();
145
+ expect(entries[1].derivedDiff?.previousVersion).toBeNull();
146
+ });
147
+ });
148
+
149
+ describe("hasMeaningfulDerivedDiff", () => {
150
+ it("detects when a derived diff contains additions or removals", () => {
151
+ expect(
152
+ hasMeaningfulDerivedDiff({
153
+ previousVersion: 1,
154
+ lines: [
155
+ { kind: "context", value: "Alpha" },
156
+ { kind: "added", value: "Beta" },
157
+ ],
158
+ })
159
+ ).toBe(true);
160
+ });
161
+
162
+ it("returns false for null or unchanged diffs", () => {
163
+ expect(hasMeaningfulDerivedDiff(null)).toBe(false);
164
+ expect(
165
+ hasMeaningfulDerivedDiff({
166
+ previousVersion: 1,
167
+ lines: [{ kind: "context", value: "Alpha" }],
168
+ })
169
+ ).toBe(false);
170
+ });
171
+ });
@@ -0,0 +1,150 @@
1
+ import type { LearnedContextRow } from "@/lib/db/schema";
2
+
3
+ export type LearnedContextSnapshotType =
4
+ | "approved"
5
+ | "rollback"
6
+ | "summarization";
7
+
8
+ export type LearnedContextDiffKind = "context" | "added" | "removed";
9
+
10
+ export interface LearnedContextDiffLine {
11
+ kind: LearnedContextDiffKind;
12
+ value: string;
13
+ }
14
+
15
+ export interface LearnedContextDerivedDiff {
16
+ previousVersion: number | null;
17
+ lines: LearnedContextDiffLine[];
18
+ }
19
+
20
+ export interface LearnedContextHistoryEntry {
21
+ row: LearnedContextRow;
22
+ snapshotContent: string | null;
23
+ derivedDiff: LearnedContextDerivedDiff | null;
24
+ }
25
+
26
+ const SNAPSHOT_CHANGE_TYPES = new Set<LearnedContextSnapshotType>([
27
+ "approved",
28
+ "rollback",
29
+ "summarization",
30
+ ]);
31
+
32
+ export function isSnapshotVersion(
33
+ row: LearnedContextRow
34
+ ): row is LearnedContextRow & {
35
+ changeType: LearnedContextSnapshotType;
36
+ content: string;
37
+ } {
38
+ return (
39
+ SNAPSHOT_CHANGE_TYPES.has(row.changeType as LearnedContextSnapshotType) &&
40
+ typeof row.content === "string"
41
+ );
42
+ }
43
+
44
+ function normalizeLines(content: string | null): string[] {
45
+ if (!content) return [];
46
+ return content.replace(/\r\n/g, "\n").split("\n");
47
+ }
48
+
49
+ function buildLcsTable(previous: string[], next: string[]): number[][] {
50
+ const table = Array.from({ length: previous.length + 1 }, () =>
51
+ Array(next.length + 1).fill(0)
52
+ );
53
+
54
+ for (let i = previous.length - 1; i >= 0; i -= 1) {
55
+ for (let j = next.length - 1; j >= 0; j -= 1) {
56
+ if (previous[i] === next[j]) {
57
+ table[i][j] = table[i + 1][j + 1] + 1;
58
+ } else {
59
+ table[i][j] = Math.max(table[i + 1][j], table[i][j + 1]);
60
+ }
61
+ }
62
+ }
63
+
64
+ return table;
65
+ }
66
+
67
+ export function buildUnifiedDiff(
68
+ previousContent: string | null,
69
+ nextContent: string
70
+ ): LearnedContextDiffLine[] {
71
+ const previousLines = normalizeLines(previousContent);
72
+ const nextLines = normalizeLines(nextContent);
73
+
74
+ if (previousLines.length === 0) {
75
+ return nextLines.map((value) => ({ kind: "added", value }));
76
+ }
77
+
78
+ const table = buildLcsTable(previousLines, nextLines);
79
+ const lines: LearnedContextDiffLine[] = [];
80
+
81
+ let i = 0;
82
+ let j = 0;
83
+
84
+ while (i < previousLines.length && j < nextLines.length) {
85
+ if (previousLines[i] === nextLines[j]) {
86
+ lines.push({ kind: "context", value: previousLines[i] });
87
+ i += 1;
88
+ j += 1;
89
+ continue;
90
+ }
91
+
92
+ if (table[i + 1][j] >= table[i][j + 1]) {
93
+ lines.push({ kind: "removed", value: previousLines[i] });
94
+ i += 1;
95
+ continue;
96
+ }
97
+
98
+ lines.push({ kind: "added", value: nextLines[j] });
99
+ j += 1;
100
+ }
101
+
102
+ while (i < previousLines.length) {
103
+ lines.push({ kind: "removed", value: previousLines[i] });
104
+ i += 1;
105
+ }
106
+
107
+ while (j < nextLines.length) {
108
+ lines.push({ kind: "added", value: nextLines[j] });
109
+ j += 1;
110
+ }
111
+
112
+ return lines;
113
+ }
114
+
115
+ export function hasMeaningfulDerivedDiff(
116
+ diff: LearnedContextDerivedDiff | null
117
+ ): boolean {
118
+ return Boolean(
119
+ diff?.lines.some((line) => line.kind === "added" || line.kind === "removed")
120
+ );
121
+ }
122
+
123
+ export function buildLearnedContextHistoryEntries(
124
+ history: LearnedContextRow[]
125
+ ): LearnedContextHistoryEntry[] {
126
+ const derivedDiffById = new Map<string, LearnedContextDerivedDiff>();
127
+ let previousSnapshot: (LearnedContextRow & {
128
+ changeType: LearnedContextSnapshotType;
129
+ content: string;
130
+ }) | null = null;
131
+
132
+ const ascending = [...history].sort((a, b) => a.version - b.version);
133
+
134
+ for (const row of ascending) {
135
+ if (!isSnapshotVersion(row)) continue;
136
+
137
+ derivedDiffById.set(row.id, {
138
+ previousVersion: previousSnapshot?.version ?? null,
139
+ lines: buildUnifiedDiff(previousSnapshot?.content ?? null, row.content),
140
+ });
141
+
142
+ previousSnapshot = row;
143
+ }
144
+
145
+ return history.map((row) => ({
146
+ row,
147
+ snapshotContent: isSnapshotVersion(row) ? row.content : null,
148
+ derivedDiff: derivedDiffById.get(row.id) ?? null,
149
+ }));
150
+ }
@@ -99,24 +99,18 @@ describe("updateOpenAISettingsSchema", () => {
99
99
  });
100
100
 
101
101
  describe("updateBudgetPolicySchema", () => {
102
- it("accepts nullable budget caps for all windows", () => {
102
+ it("accepts a monthly-only budget payload", () => {
103
103
  const result = updateBudgetPolicySchema.safeParse({
104
104
  overall: {
105
- dailySpendCapUsd: null,
106
105
  monthlySpendCapUsd: 50,
107
106
  },
108
107
  runtimes: {
109
108
  "claude-code": {
110
- dailySpendCapUsd: 10,
111
109
  monthlySpendCapUsd: 100,
112
- dailyTokenCap: 10000,
113
- monthlyTokenCap: null,
110
+ claudeOAuthPlan: "max_5x",
114
111
  },
115
112
  "openai-codex-app-server": {
116
- dailySpendCapUsd: null,
117
113
  monthlySpendCapUsd: null,
118
- dailyTokenCap: null,
119
- monthlyTokenCap: 50000,
120
114
  },
121
115
  },
122
116
  });
@@ -127,21 +121,34 @@ describe("updateBudgetPolicySchema", () => {
127
121
  it("rejects zero and negative values", () => {
128
122
  const result = updateBudgetPolicySchema.safeParse({
129
123
  overall: {
130
- dailySpendCapUsd: 0,
131
124
  monthlySpendCapUsd: -1,
132
125
  },
133
126
  runtimes: {
134
127
  "claude-code": {
135
- dailySpendCapUsd: null,
136
- monthlySpendCapUsd: null,
137
- dailyTokenCap: 0,
138
- monthlyTokenCap: null,
128
+ monthlySpendCapUsd: 0,
129
+ claudeOAuthPlan: "pro",
139
130
  },
140
131
  "openai-codex-app-server": {
141
- dailySpendCapUsd: null,
142
132
  monthlySpendCapUsd: null,
143
- dailyTokenCap: null,
144
- monthlyTokenCap: null,
133
+ },
134
+ },
135
+ });
136
+
137
+ expect(result.success).toBe(false);
138
+ });
139
+
140
+ it("rejects invalid Claude OAuth plans", () => {
141
+ const result = updateBudgetPolicySchema.safeParse({
142
+ overall: {
143
+ monthlySpendCapUsd: 300,
144
+ },
145
+ runtimes: {
146
+ "claude-code": {
147
+ monthlySpendCapUsd: 150,
148
+ claudeOAuthPlan: "enterprise",
149
+ },
150
+ "openai-codex-app-server": {
151
+ monthlySpendCapUsd: 150,
145
152
  },
146
153
  },
147
154
  });
@@ -25,22 +25,15 @@ const nullablePositiveNumber = z.preprocess((value) => {
25
25
  return value;
26
26
  }, z.number().finite().positive().nullable());
27
27
 
28
- const nullablePositiveInteger = z.preprocess((value) => {
29
- if (value === "" || value == null) return null;
30
- if (typeof value === "string") return Number(value);
31
- return value;
32
- }, z.number().int().positive().nullable());
28
+ export const claudeOAuthPlanSchema = z.enum(["pro", "max_5x", "max_20x"]);
33
29
 
34
30
  export const runtimeBudgetPolicySchema = z.object({
35
- dailySpendCapUsd: nullablePositiveNumber,
36
31
  monthlySpendCapUsd: nullablePositiveNumber,
37
- dailyTokenCap: nullablePositiveInteger,
38
- monthlyTokenCap: nullablePositiveInteger,
32
+ claudeOAuthPlan: claudeOAuthPlanSchema.optional(),
39
33
  });
40
34
 
41
35
  export const budgetPolicySchema = z.object({
42
36
  overall: z.object({
43
- dailySpendCapUsd: nullablePositiveNumber,
44
37
  monthlySpendCapUsd: nullablePositiveNumber,
45
38
  }),
46
39
  runtimes: z.object(
@@ -55,3 +48,4 @@ export const updateBudgetPolicySchema = budgetPolicySchema;
55
48
  export type RuntimeBudgetPolicy = z.infer<typeof runtimeBudgetPolicySchema>;
56
49
  export type BudgetPolicy = z.infer<typeof budgetPolicySchema>;
57
50
  export type UpdateBudgetPolicyInput = z.infer<typeof updateBudgetPolicySchema>;
51
+ export type ClaudeOAuthPlan = z.infer<typeof claudeOAuthPlanSchema>;
@@ -16,6 +16,10 @@ import {
16
16
  buildSwarmWorkerPrompt,
17
17
  getSwarmWorkflowStructure,
18
18
  } from "./swarm";
19
+ import {
20
+ openLearningSession,
21
+ closeLearningSession,
22
+ } from "@/lib/agents/learning-session";
19
23
 
20
24
  /**
21
25
  * Execute a workflow by advancing through its steps according to the pattern.
@@ -43,6 +47,10 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
43
47
  timestamp: new Date(),
44
48
  });
45
49
 
50
+ // Open a learning session to buffer context proposals during execution.
51
+ // Proposals are collected and presented as a single batch at workflow end.
52
+ openLearningSession(workflowId);
53
+
46
54
  // Loop pattern manages its own lifecycle — delegate fully
47
55
  if (definition.pattern === "loop") {
48
56
  try {
@@ -72,6 +80,11 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
72
80
  }),
73
81
  timestamp: new Date(),
74
82
  });
83
+ } finally {
84
+ // Close learning session — flush buffered proposals as batch notification
85
+ await closeLearningSession(workflowId).catch((err) => {
86
+ console.error("[workflow-engine] Failed to close learning session:", err);
87
+ });
75
88
  }
76
89
  return;
77
90
  }
@@ -128,6 +141,11 @@ export async function executeWorkflow(workflowId: string): Promise<void> {
128
141
  }),
129
142
  timestamp: new Date(),
130
143
  });
144
+ } finally {
145
+ // Close learning session — flush buffered proposals as batch notification
146
+ await closeLearningSession(workflowId).catch((err) => {
147
+ console.error("[workflow-engine] Failed to close learning session:", err);
148
+ });
131
149
  }
132
150
  }
133
151