@loopstack/agent-examples 0.1.1

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 (53) hide show
  1. package/README.md +150 -0
  2. package/dist/agent-examples.module.d.ts +3 -0
  3. package/dist/agent-examples.module.d.ts.map +1 -0
  4. package/dist/agent-examples.module.js +52 -0
  5. package/dist/agent-examples.module.js.map +1 -0
  6. package/dist/index.d.ts +6 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +22 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/tools/calculator.tool.d.ts +15 -0
  11. package/dist/tools/calculator.tool.d.ts.map +1 -0
  12. package/dist/tools/calculator.tool.js +51 -0
  13. package/dist/tools/calculator.tool.js.map +1 -0
  14. package/dist/tools/index.d.ts +3 -0
  15. package/dist/tools/index.d.ts.map +1 -0
  16. package/dist/tools/index.js +19 -0
  17. package/dist/tools/index.js.map +1 -0
  18. package/dist/tools/weather-lookup.tool.d.ts +10 -0
  19. package/dist/tools/weather-lookup.tool.d.ts.map +1 -0
  20. package/dist/tools/weather-lookup.tool.js +36 -0
  21. package/dist/tools/weather-lookup.tool.js.map +1 -0
  22. package/dist/workflows/agent/agent-example.workflow.d.ts +9 -0
  23. package/dist/workflows/agent/agent-example.workflow.d.ts.map +1 -0
  24. package/dist/workflows/agent/agent-example.workflow.js +62 -0
  25. package/dist/workflows/agent/agent-example.workflow.js.map +1 -0
  26. package/dist/workflows/agent/templates/system.md +4 -0
  27. package/dist/workflows/code-agent/code-agent-example.workflow.d.ts +12 -0
  28. package/dist/workflows/code-agent/code-agent-example.workflow.d.ts.map +1 -0
  29. package/dist/workflows/code-agent/code-agent-example.workflow.js +75 -0
  30. package/dist/workflows/code-agent/code-agent-example.workflow.js.map +1 -0
  31. package/dist/workflows/custom-agent/custom-agent-example.workflow.d.ts +30 -0
  32. package/dist/workflows/custom-agent/custom-agent-example.workflow.d.ts.map +1 -0
  33. package/dist/workflows/custom-agent/custom-agent-example.workflow.js +165 -0
  34. package/dist/workflows/custom-agent/custom-agent-example.workflow.js.map +1 -0
  35. package/dist/workflows/custom-agent/templates/system.md +14 -0
  36. package/dist/workflows/custom-agent/templates/wrap-up.md +3 -0
  37. package/dist/workflows/mcp-linear/mcp-linear-example.workflow.d.ts +18 -0
  38. package/dist/workflows/mcp-linear/mcp-linear-example.workflow.d.ts.map +1 -0
  39. package/dist/workflows/mcp-linear/mcp-linear-example.workflow.js +65 -0
  40. package/dist/workflows/mcp-linear/mcp-linear-example.workflow.js.map +1 -0
  41. package/package.json +54 -0
  42. package/src/agent-examples.module.ts +40 -0
  43. package/src/index.ts +5 -0
  44. package/src/tools/calculator.tool.ts +47 -0
  45. package/src/tools/index.ts +2 -0
  46. package/src/tools/weather-lookup.tool.ts +29 -0
  47. package/src/workflows/agent/agent-example.workflow.ts +40 -0
  48. package/src/workflows/agent/templates/system.md +4 -0
  49. package/src/workflows/code-agent/code-agent-example.workflow.ts +56 -0
  50. package/src/workflows/custom-agent/custom-agent-example.workflow.ts +202 -0
  51. package/src/workflows/custom-agent/templates/system.md +14 -0
  52. package/src/workflows/custom-agent/templates/wrap-up.md +3 -0
  53. package/src/workflows/mcp-linear/mcp-linear-example.workflow.ts +52 -0
@@ -0,0 +1,202 @@
1
+ import { join } from 'node:path';
2
+ import { BaseWorkflow, Guard, MarkdownDocument, Transition, Workflow } from '@loopstack/common';
3
+ import type { TransitionInput } from '@loopstack/common';
4
+ import type { LlmDelegateResult, LlmGenerateTextResult } from '@loopstack/llm-provider-module';
5
+ import {
6
+ LlmDelegateToolCallsTool,
7
+ LlmGenerateTextTool,
8
+ LlmMessageDocument,
9
+ LlmUpdateToolResultTool,
10
+ } from '@loopstack/llm-provider-module';
11
+
12
+ const MAX_TURNS = 2;
13
+
14
+ interface CustomAgentState {
15
+ llmResult?: LlmGenerateTextResult;
16
+ delegateResult?: LlmDelegateResult;
17
+ turnCount: number;
18
+ }
19
+
20
+ /**
21
+ * Custom agent loop with a deterministic turn budget and a forced wrap-up phase.
22
+ *
23
+ * The generic `AgentWorkflow` only exits when the LLM emits `end_turn`. This example
24
+ * shows what a hand-rolled loop unlocks: after `MAX_TURNS` tool-using turns, the
25
+ * workflow stops the loop itself and runs one final LLM call with a different
26
+ * system prompt and no tools, asking the model to summarize whatever it has so far.
27
+ *
28
+ * Two distinct LLM phases (loop with tools → wrap-up without tools) cannot be
29
+ * expressed by a single `AgentWorkflow.run(...)` call, which is the motivation for
30
+ * building a custom agent.
31
+ */
32
+ @Workflow({
33
+ title: 'Agent - Custom Agent Example',
34
+ description:
35
+ 'A from-scratch agent loop with a deterministic turn budget. After N tool-using turns the workflow forces a wrap-up LLM call with a different system prompt and no tools — illustrating a control-flow shape the generic AgentWorkflow cannot express.',
36
+ })
37
+ export class CustomAgentExampleWorkflow extends BaseWorkflow {
38
+ constructor(
39
+ private readonly llmGenerateText: LlmGenerateTextTool,
40
+ private readonly llmDelegateToolCalls: LlmDelegateToolCallsTool,
41
+ private readonly llmUpdateToolResult: LlmUpdateToolResultTool,
42
+ ) {
43
+ super();
44
+ }
45
+
46
+ /**
47
+ * Initial transition. Renders an introductory note for the user, seeds the
48
+ * conversation with the user's request, and initialises the turn counter.
49
+ */
50
+ @Transition({ to: 'ready' })
51
+ async setup(_state: CustomAgentState) {
52
+ await this.documentStore.save(MarkdownDocument, {
53
+ markdown:
54
+ '# Custom Agent Example\n\n' +
55
+ `This agent has a budget of **${MAX_TURNS} tool-using turns**. ` +
56
+ 'Once the budget is hit, the workflow forces a final summary turn with no tools available.',
57
+ });
58
+
59
+ await this.documentStore.save(LlmMessageDocument, {
60
+ role: 'user',
61
+ text:
62
+ 'I want to travel somewhere warm. Find me the first city of at least 25°C from this list, ' +
63
+ 'trying them one at a time in order: London, New York, Paris, Berlin, Tokyo. ' +
64
+ 'After each lookup, decide based on the temperature whether to keep searching.',
65
+ });
66
+
67
+ this.assignState({ turnCount: 0 });
68
+ }
69
+
70
+ /**
71
+ * Main loop turn. While the budget allows it, runs a normal LLM call with the
72
+ * full tool set and increments the turn counter. The next state is decided
73
+ * by the guards on transitions out of `prompt_executed`.
74
+ */
75
+ @Transition({ from: 'ready', to: 'prompt_executed', priority: 10 })
76
+ @Guard('hasBudget')
77
+ async llmTurn(state: CustomAgentState) {
78
+ const result = await this.llmGenerateText.call(
79
+ {},
80
+ {
81
+ config: {
82
+ provider: 'claude',
83
+ system: this.render(join(__dirname, 'templates', 'system.md')),
84
+ tools: ['weather_lookup', 'calculator'],
85
+ },
86
+ },
87
+ );
88
+
89
+ this.assignState({
90
+ turnCount: state.turnCount + 1,
91
+ llmResult: result.data,
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Fires the moment the budget is exhausted. Appends a user-role
97
+ * `LlmMessageDocument` telling the model the budget is gone (so the model
98
+ * itself sees the constraint, not just the system prompt). Kept as its own
99
+ * short transition so the message renders in the UI immediately, without
100
+ * waiting for the slower wrap-up LLM call that follows.
101
+ */
102
+ @Transition({ from: 'ready', to: 'over_budget' })
103
+ @Guard('isOverBudget')
104
+ async notifyOverBudget(_state: CustomAgentState) {
105
+ await this.documentStore.save(LlmMessageDocument, {
106
+ role: 'user',
107
+ text:
108
+ `You have used your full turn budget of ${MAX_TURNS} tool-using turns and can no longer call tools. ` +
109
+ 'Summarise what you found so far in plain text and answer based on that.',
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Forced final turn after the budget notice has rendered. Runs one last LLM
115
+ * call with the wrap-up system prompt and no tools, asking for a plain-text
116
+ * summary based on whatever results were gathered before the budget hit.
117
+ */
118
+ @Transition({ from: 'over_budget', to: 'end' })
119
+ async wrapUp(_state: CustomAgentState) {
120
+ await this.llmGenerateText.call(
121
+ {},
122
+ {
123
+ config: {
124
+ provider: 'claude',
125
+ system: this.render(join(__dirname, 'templates', 'wrap-up.md')),
126
+ tools: [],
127
+ },
128
+ },
129
+ );
130
+ }
131
+
132
+ /**
133
+ * The LLM asked to call one or more tools. Hands the assistant message to
134
+ * `LlmDelegateToolCalls`, which dispatches each tool concurrently and reports
135
+ * back via the `toolResultReceived` callback.
136
+ */
137
+ @Transition({ from: 'prompt_executed', to: 'awaiting_tools', priority: 10 })
138
+ @Guard('hasToolCalls')
139
+ async executeToolCalls(state: CustomAgentState) {
140
+ const result = await this.llmDelegateToolCalls.call({
141
+ message: state.llmResult!.message,
142
+ callback: { transition: 'toolResultReceived' },
143
+ });
144
+ this.assignState({ delegateResult: result.data });
145
+ }
146
+
147
+ /**
148
+ * Wait-transition fired once per completed tool. Folds the result back into
149
+ * the `delegateResult` aggregate so we know which tool_use ids are still
150
+ * outstanding and when every call has finished.
151
+ */
152
+ @Transition({ from: 'awaiting_tools', to: 'awaiting_tools', wait: true })
153
+ async toolResultReceived(state: CustomAgentState, input: TransitionInput) {
154
+ const result = await this.llmUpdateToolResult.call({
155
+ delegateResult: state.delegateResult!,
156
+ completedTool: input,
157
+ });
158
+ this.assignState({ delegateResult: result.data });
159
+ }
160
+
161
+ /**
162
+ * Every tool dispatched in this turn has reported back. Hands control back
163
+ * to `ready` so the loop can decide whether to keep going or wrap up.
164
+ */
165
+ @Transition({ from: 'awaiting_tools', to: 'ready' })
166
+ @Guard('allToolsComplete')
167
+ toolsComplete(_state: CustomAgentState) {}
168
+
169
+ /**
170
+ * The LLM emitted `end_turn` instead of more tool calls — it considers the
171
+ * task finished. The text response was already persisted by `llmTurn`, so we
172
+ * just close the workflow.
173
+ */
174
+ @Transition({ from: 'prompt_executed', to: 'end' })
175
+ @Guard('isEndTurn')
176
+ respond(_state: CustomAgentState) {}
177
+
178
+ /** True while the model is still allowed to take another tool-using turn. */
179
+ private hasBudget(state: CustomAgentState): boolean {
180
+ return state.turnCount < MAX_TURNS;
181
+ }
182
+
183
+ /** Inverse of {@link hasBudget} — gates the forced `wrapUp` transition. */
184
+ private isOverBudget(state: CustomAgentState): boolean {
185
+ return state.turnCount >= MAX_TURNS;
186
+ }
187
+
188
+ /** True when the last LLM response asked to call one or more tools. */
189
+ private hasToolCalls(state: CustomAgentState): boolean {
190
+ return state.llmResult?.message.stopReason === 'tool_use';
191
+ }
192
+
193
+ /** True when every tool dispatched in the current turn has reported back. */
194
+ private allToolsComplete(state: CustomAgentState): boolean {
195
+ return !!state.delegateResult?.allCompleted;
196
+ }
197
+
198
+ /** True when the last LLM response was a final plain-text answer (no tools). */
199
+ private isEndTurn(state: CustomAgentState): boolean {
200
+ return state.llmResult?.message.stopReason === 'end_turn';
201
+ }
202
+ }
@@ -0,0 +1,14 @@
1
+ You are a helpful assistant searching for a city that matches the user's criteria.
2
+
3
+ Available tools:
4
+
5
+ - `weather_lookup` — look up the current weather for a city
6
+ - `calculator` — perform a basic arithmetic calculation
7
+
8
+ How to work:
9
+
10
+ - Look up exactly ONE city per turn — never call `weather_lookup` for multiple cities in parallel.
11
+ - After each lookup, reason about the result before deciding whether to keep searching.
12
+ - Stop calling tools as soon as you have enough information to answer.
13
+
14
+ When you are done, answer in plain text without calling any more tools.
@@ -0,0 +1,3 @@
1
+ You have reached your turn budget and can no longer call tools.
2
+
3
+ Summarize what you have found so far for the user in plain text. If you did not finish gathering every data point, acknowledge that and answer based on what you have. Do not call any tools.
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+ import { ChatAgentWorkflow } from '@loopstack/agent';
3
+ import { BaseWorkflow, Transition, Workflow } from '@loopstack/common';
4
+ import type { RunContext } from '@loopstack/common';
5
+ import { McpCallTool, McpListToolsTool } from '@loopstack/mcp-module';
6
+
7
+ const LINEAR_MCP_URL = 'https://mcp.linear.app/mcp';
8
+
9
+ const McpLinearExampleArgsSchema = z.object({
10
+ initialMessage: z
11
+ .string()
12
+ .optional()
13
+ .default(
14
+ 'List the available Linear MCP tools, then fetch my active issues and summarize the top 5 by priority with assignee and status.',
15
+ )
16
+ .describe('Initial message shown to the agent.'),
17
+ });
18
+
19
+ type McpLinearExampleArgs = z.infer<typeof McpLinearExampleArgsSchema>;
20
+
21
+ @Workflow({
22
+ title: 'Agent - MCP Linear Example',
23
+ description: "Chat agent connected to Linear's hosted MCP server. Demonstrates MCP tool integration.",
24
+ schema: McpLinearExampleArgsSchema,
25
+ })
26
+ export class McpLinearExampleWorkflow extends BaseWorkflow<McpLinearExampleArgs> {
27
+ constructor(
28
+ private readonly chatAgentWorkflow: ChatAgentWorkflow,
29
+ private readonly mcpListTools: McpListToolsTool,
30
+ private readonly mcpCallTool: McpCallTool,
31
+ ) {
32
+ super();
33
+ }
34
+
35
+ @Transition({ to: 'chatting' })
36
+ async startChat(state: Record<string, unknown>, ctx: RunContext<McpLinearExampleArgs>) {
37
+ const systemPrompt = [
38
+ `You are a Linear assistant connected via MCP at ${LINEAR_MCP_URL} (transport: streamableHttp).`,
39
+ 'Use `mcpListTools` to discover the available Linear tools, then `mcpCallTool` to invoke them.',
40
+ `Always pass serverUrl="${LINEAR_MCP_URL}" and transport="streamableHttp".`,
41
+ ].join('\n');
42
+
43
+ await this.chatAgentWorkflow.run(
44
+ {
45
+ system: systemPrompt,
46
+ tools: ['mcp_list_tools', 'mcp_call'],
47
+ userMessage: ctx.args.initialMessage,
48
+ },
49
+ { show: 'inline', label: 'Linear Agent Chat' },
50
+ );
51
+ }
52
+ }