glove-foundry 0.0.0 → 0.2.0
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/dist/{chunk-UNHGCCSA.js → chunk-P65RT7H5.js} +486 -49
- package/dist/cli.js +536 -352
- package/dist/index.js +1 -1
- package/docs/building-with-foundry.md +39 -2
- package/docs/inspector.md +35 -2
- package/package.json +7 -6
- package/templates/minimal/README.md +58 -0
- package/templates/minimal/agents/assistant/agent.ts +28 -0
- package/templates/minimal/agents/assistant/tools/current-time.tool.ts +17 -0
- package/templates/minimal/env.example +2 -0
- package/templates/minimal/foundry.application.ts +12 -0
- package/templates/minimal/foundry.config.ts +6 -0
- package/templates/minimal/gitignore +3 -0
- package/templates/travel-concierge/README.md +203 -0
- package/templates/travel-concierge/agents/concierge/actions/reply.action.ts +6 -0
- package/templates/travel-concierge/agents/concierge/agent.ts +106 -0
- package/templates/travel-concierge/agents/concierge/apps/calendar.app.ts +67 -0
- package/templates/travel-concierge/agents/concierge/composition.ts +18 -0
- package/templates/travel-concierge/agents/concierge/events/message-received.event.ts +7 -0
- package/templates/travel-concierge/agents/concierge/events/message-sent.event.ts +7 -0
- package/templates/travel-concierge/agents/concierge/layers/trip-context.layer.ts +23 -0
- package/templates/travel-concierge/agents/concierge/memory/traveller.memory.ts +19 -0
- package/templates/travel-concierge/agents/concierge/predicates/mentions-trip.predicate.ts +17 -0
- package/templates/travel-concierge/agents/concierge/schedules/trip-countdown.ts +16 -0
- package/templates/travel-concierge/agents/concierge/subscribers/usage.subscriber.ts +16 -0
- package/templates/travel-concierge/agents/concierge/tools/find-flights.tool.ts +49 -0
- package/templates/travel-concierge/agents/concierge/topology.ts +35 -0
- package/templates/travel-concierge/agents/concierge/transmissions/messaging.transmission.ts +57 -0
- package/templates/travel-concierge/agents/concierge/workbench.ts +26 -0
- package/templates/travel-concierge/env.example +3 -0
- package/templates/travel-concierge/foundry.application.ts +18 -0
- package/templates/travel-concierge/foundry.config.ts +9 -0
- package/templates/travel-concierge/gitignore +3 -0
- package/templates/travel-concierge/lib/demo-model.ts +72 -0
- package/templates/travel-concierge/src/client.ts +22 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { defineApp } from "glove-foundry";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import messaging from "../transmissions/messaging.transmission.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An application is a capability an *instance* installs, not something the
|
|
8
|
+
* definition always has. Two concierge instances can run the same code with
|
|
9
|
+
* different calendars, or with none at all.
|
|
10
|
+
*
|
|
11
|
+
* `config` is validated when the instance installs it, so `install` receives a
|
|
12
|
+
* typed value. Swap the in-memory `HELD` map for a real calendar API.
|
|
13
|
+
*/
|
|
14
|
+
const HELD = new Map<string, { readonly start: string; readonly end: string }>();
|
|
15
|
+
|
|
16
|
+
const calendar = defineApp({
|
|
17
|
+
description: "Read a traveller's calendar and hold provisional trip dates",
|
|
18
|
+
config: z.object({
|
|
19
|
+
calendarId: z.string().default("primary"),
|
|
20
|
+
timezone: z.string().default("UTC"),
|
|
21
|
+
}),
|
|
22
|
+
// Declaring the transmission here lets an installed instance send messages.
|
|
23
|
+
inbound: [messaging],
|
|
24
|
+
outbound: [messaging],
|
|
25
|
+
install: ({ config }) => Effect.succeed({
|
|
26
|
+
tools: [
|
|
27
|
+
{
|
|
28
|
+
name: "calendar_check_availability",
|
|
29
|
+
description: "Check whether a date range is free on the traveller's calendar",
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
start: z.string().describe("ISO start date, for example 2026-04-02"),
|
|
32
|
+
end: z.string().describe("ISO end date, for example 2026-04-09"),
|
|
33
|
+
}),
|
|
34
|
+
async do({ start, end }) {
|
|
35
|
+
const clash = [...HELD.values()].find((hold) => hold.start <= end && start <= hold.end);
|
|
36
|
+
return {
|
|
37
|
+
status: "success" as const,
|
|
38
|
+
data: {
|
|
39
|
+
calendarId: config.calendarId,
|
|
40
|
+
timezone: config.timezone,
|
|
41
|
+
available: !clash,
|
|
42
|
+
...(clash ? { conflictsWith: clash } : {}),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "calendar_hold_dates",
|
|
49
|
+
description: "Place a provisional hold on a date range while the trip is being planned",
|
|
50
|
+
inputSchema: z.object({
|
|
51
|
+
label: z.string().describe("What the hold is for, for example 'Nairobi trip'"),
|
|
52
|
+
start: z.string(),
|
|
53
|
+
end: z.string(),
|
|
54
|
+
}),
|
|
55
|
+
async do({ label, start, end }) {
|
|
56
|
+
HELD.set(label, { start, end });
|
|
57
|
+
return {
|
|
58
|
+
status: "success" as const,
|
|
59
|
+
data: { held: true, label, start, end, calendarId: config.calendarId },
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export default calendar;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { composeAgent } from "glove-foundry";
|
|
2
|
+
import calendar from "./apps/calendar.app.js";
|
|
3
|
+
import tripContext from "./layers/trip-context.layer.js";
|
|
4
|
+
import travellerMemory from "./memory/traveller.memory.js";
|
|
5
|
+
import usage from "./subscribers/usage.subscriber.js";
|
|
6
|
+
import findFlights from "./tools/find-flights.tool.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Composition is by value, not by string id. Rename a file and the import
|
|
10
|
+
* breaks at compile time instead of failing at runtime.
|
|
11
|
+
*/
|
|
12
|
+
export const conciergeComponents = composeAgent(
|
|
13
|
+
calendar,
|
|
14
|
+
findFlights,
|
|
15
|
+
travellerMemory,
|
|
16
|
+
tripContext,
|
|
17
|
+
usage,
|
|
18
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { defineLayer } from "glove-foundry";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A layer reaches the native Glove runtime directly. Use it when you need
|
|
6
|
+
* something a tool cannot express — here, a skill the model can read without
|
|
7
|
+
* spending a tool call.
|
|
8
|
+
*/
|
|
9
|
+
const tripContext = defineLayer({
|
|
10
|
+
description: "Expose the current Foundry run identity to the agent",
|
|
11
|
+
setup: ({ glove, agentId, runId, history }) => Effect.sync(() => {
|
|
12
|
+
glove.defineSkill({
|
|
13
|
+
name: "trip-context",
|
|
14
|
+
description: "Where this conversation stands",
|
|
15
|
+
exposeToAgent: true,
|
|
16
|
+
async handler() {
|
|
17
|
+
return `agent=${agentId} run=${runId} priorMessages=${history.length}`;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default tripContext;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { defineMemory } from "glove-foundry";
|
|
3
|
+
import { MemorySchema } from "glove-memory/core";
|
|
4
|
+
import { InMemoryContextAdapter } from "glove-memory/in-memory";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ambient context the concierge should carry between conversations: seat
|
|
8
|
+
* preference, home airport, who is travelling. The adapter is replaceable —
|
|
9
|
+
* point it at a durable store when you outgrow the in-memory one.
|
|
10
|
+
*/
|
|
11
|
+
const schema = new MemorySchema();
|
|
12
|
+
const context = new InMemoryContextAdapter({ schema });
|
|
13
|
+
|
|
14
|
+
const travellerMemory = defineMemory({
|
|
15
|
+
description: "What we already know about this traveller",
|
|
16
|
+
context: { adapter: () => Effect.succeed(context) },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default travellerMemory;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { defineTransmissionPredicate } from "glove-foundry";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Predicates keep routing decisions out of the agent. A playbook references
|
|
6
|
+
* this by value and supplies `parameters` at match time, so one predicate
|
|
7
|
+
* serves many rules.
|
|
8
|
+
*/
|
|
9
|
+
const mentionsTrip = defineTransmissionPredicate({
|
|
10
|
+
match: (event: { readonly text: string }, parameters) => {
|
|
11
|
+
const needles = Array.isArray(parameters.any) ? parameters.any : ["trip", "flight", "travel"];
|
|
12
|
+
const text = event.text.toLowerCase();
|
|
13
|
+
return Effect.succeed(needles.some((needle) => text.includes(String(needle).toLowerCase())));
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export default mentionsTrip;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineSchedule } from "glove-foundry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agents never call setTimeout. A schedule is a definition; Foundry persists
|
|
5
|
+
* the activation and wakes the agent with this message. Watch it appear under
|
|
6
|
+
* Automations in the inspector.
|
|
7
|
+
*/
|
|
8
|
+
const tripCountdown = defineSchedule({
|
|
9
|
+
name: "trip-countdown",
|
|
10
|
+
description: "Check every weekday whether an upcoming trip still needs work.",
|
|
11
|
+
timing: { kind: "cron", expression: "0 8 * * 1-5", timezone: "UTC" },
|
|
12
|
+
message:
|
|
13
|
+
"Review any held trip dates and unconfirmed bookings. If something needs the traveller's decision, say exactly what and why.",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default tripCountdown;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineSubscriber } from "glove-foundry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subscribers observe without changing behaviour. Everything they see is also
|
|
5
|
+
* in the inspector's event trace; this is the seam for shipping it elsewhere.
|
|
6
|
+
*/
|
|
7
|
+
const usage = defineSubscriber({
|
|
8
|
+
description: "Report token consumption for each run",
|
|
9
|
+
create: {
|
|
10
|
+
async record(type, data) {
|
|
11
|
+
if (type === "token_consumption") process.stdout.write(`[tokens] ${JSON.stringify(data)}\n`);
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default usage;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineSharedTool } from "glove-foundry";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A shared tool is the smallest Foundry capability: one file, one default
|
|
6
|
+
* export, and an id taken from the filename (`find-flights`). Nothing here
|
|
7
|
+
* knows which agent will mount it.
|
|
8
|
+
*
|
|
9
|
+
* The sample data is deliberately local so a fresh project runs offline.
|
|
10
|
+
* Replace `search` with a call to your own inventory or a provider API.
|
|
11
|
+
*/
|
|
12
|
+
const SAMPLE_FLIGHTS = [
|
|
13
|
+
{ carrier: "Kestrel Air", from: "LIS", to: "NBO", depart: "08:40", hours: 9.5, price: 612 },
|
|
14
|
+
{ carrier: "Meridian", from: "LIS", to: "NBO", depart: "13:15", hours: 11, price: 494 },
|
|
15
|
+
{ carrier: "Northwind", from: "LIS", to: "NBO", depart: "22:05", hours: 9, price: 738 },
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
function search(from: string, to: string, maxPrice: number) {
|
|
19
|
+
const origin = from.trim().toUpperCase();
|
|
20
|
+
const destination = to.trim().toUpperCase();
|
|
21
|
+
return SAMPLE_FLIGHTS.filter((flight) =>
|
|
22
|
+
(!origin || flight.from === origin) &&
|
|
23
|
+
(!destination || flight.to === destination) &&
|
|
24
|
+
flight.price <= maxPrice
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const findFlights = defineSharedTool({
|
|
29
|
+
description: "Search available flights between two airports",
|
|
30
|
+
tool: {
|
|
31
|
+
name: "find_flights",
|
|
32
|
+
description:
|
|
33
|
+
"Search flights between two IATA airport codes. Returns carrier, departure time, duration and price.",
|
|
34
|
+
inputSchema: z.object({
|
|
35
|
+
from: z.string().describe("Origin IATA code, for example LIS"),
|
|
36
|
+
to: z.string().describe("Destination IATA code, for example NBO"),
|
|
37
|
+
maxPrice: z.number().default(5_000).describe("Highest acceptable price in USD"),
|
|
38
|
+
}),
|
|
39
|
+
async do({ from, to, maxPrice }) {
|
|
40
|
+
const matches = search(from, to, maxPrice);
|
|
41
|
+
if (matches.length === 0) {
|
|
42
|
+
return { status: "success" as const, data: { matches: [], note: "No flight matched those constraints." } };
|
|
43
|
+
}
|
|
44
|
+
return { status: "success" as const, data: { matches } };
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export default findFlights;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineAccount, defineInboundRoute, defineOutboundRoute } from "glove-foundry";
|
|
2
|
+
import messaging from "./transmissions/messaging.transmission.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Topology is runtime *data*, not code identity — which is why these carry
|
|
6
|
+
* explicit ids. Edit them, load them from your database, or create them from
|
|
7
|
+
* the inspector; the agent definition above does not change.
|
|
8
|
+
*/
|
|
9
|
+
export const travellerAccount = defineAccount({
|
|
10
|
+
id: "traveller-chat",
|
|
11
|
+
transmission: messaging,
|
|
12
|
+
externalAccountId: "demo-traveller",
|
|
13
|
+
label: "Demo traveller chat",
|
|
14
|
+
// The real credential stays behind this reference, inside an adapter you own.
|
|
15
|
+
accessRef: "adapter://messaging/demo-traveller",
|
|
16
|
+
metadata: { handle: "@demo_traveller", provider: "telegram" },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const travellerInbound = defineInboundRoute({
|
|
20
|
+
id: "traveller-inbound",
|
|
21
|
+
transmission: messaging,
|
|
22
|
+
account: travellerAccount,
|
|
23
|
+
visibility: "private",
|
|
24
|
+
enabled: true,
|
|
25
|
+
config: { provider: "telegram" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const travellerOutbound = defineOutboundRoute({
|
|
29
|
+
id: "traveller-outbound",
|
|
30
|
+
transmission: messaging,
|
|
31
|
+
account: travellerAccount,
|
|
32
|
+
visibility: "private",
|
|
33
|
+
enabled: true,
|
|
34
|
+
config: { provider: "telegram" },
|
|
35
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { defineTransmission } from "glove-foundry";
|
|
3
|
+
import mentionsTrip from "../predicates/mentions-trip.predicate.js";
|
|
4
|
+
import messageReceived from "../events/message-received.event.js";
|
|
5
|
+
import messageSent from "../events/message-sent.event.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A transmission is the *shape* of an external transport, not a vendor SDK.
|
|
9
|
+
* Telegram and WhatsApp both look like "a chat with threads", so one
|
|
10
|
+
* transmission covers both and `config.provider` picks which.
|
|
11
|
+
*
|
|
12
|
+
* Foundry never stores credentials. `account.metadata` holds safe descriptive
|
|
13
|
+
* fields; the secret itself lives behind `accessRef` in an adapter you own.
|
|
14
|
+
*/
|
|
15
|
+
export const sendMessage = {
|
|
16
|
+
id: "messaging:send",
|
|
17
|
+
description: "Send a message into a traveller's chat thread",
|
|
18
|
+
account: "required",
|
|
19
|
+
effect: "write",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/** Swap this for a real Bot API call. See README, "Connecting a real chat app". */
|
|
23
|
+
function deliver(input: { readonly threadId: string; readonly text: string }, provider: string) {
|
|
24
|
+
process.stdout.write(`[${provider}] -> ${input.threadId}: ${input.text}\n`);
|
|
25
|
+
return Effect.succeed({ externalMessageId: `${provider}:${input.threadId}:${Date.now()}` });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const messaging = defineTransmission({
|
|
29
|
+
name: "Messaging",
|
|
30
|
+
description: "Chat transport for Telegram, WhatsApp, or any threaded messenger",
|
|
31
|
+
account: {
|
|
32
|
+
required: true,
|
|
33
|
+
metadata: Schema.Struct({
|
|
34
|
+
handle: Schema.String,
|
|
35
|
+
provider: Schema.Literal("telegram", "whatsapp"),
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
capabilities: [sendMessage],
|
|
39
|
+
events: [messageReceived, messageSent],
|
|
40
|
+
inbound: {
|
|
41
|
+
config: Schema.Struct({ provider: Schema.Literal("telegram", "whatsapp") }),
|
|
42
|
+
event: Schema.Struct({ threadId: Schema.String, text: Schema.String }),
|
|
43
|
+
classify: () => Effect.succeed(messageReceived),
|
|
44
|
+
predicates: [mentionsTrip],
|
|
45
|
+
},
|
|
46
|
+
outbound: {
|
|
47
|
+
config: Schema.Struct({ provider: Schema.Literal("telegram", "whatsapp") }),
|
|
48
|
+
input: Schema.Struct({ threadId: Schema.String, text: Schema.String }),
|
|
49
|
+
output: Schema.Struct({ externalMessageId: Schema.String }),
|
|
50
|
+
adapter: {
|
|
51
|
+
deliver: (input, context) =>
|
|
52
|
+
deliver(input, (context.route.config as { provider?: string }).provider ?? "telegram"),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export default messaging;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { JsSession, defineFn } from "glove-js";
|
|
2
|
+
import { defineRepl, defineWorkingEnvironment } from "glove-foundry";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The workbench is the agent's own sandbox: a virtual filesystem for building
|
|
7
|
+
* up an itinerary document, and a JavaScript REPL for arithmetic the model
|
|
8
|
+
* should not do in its head (totals, date maths, currency).
|
|
9
|
+
*
|
|
10
|
+
* Delete either one if this agent does not need it.
|
|
11
|
+
*/
|
|
12
|
+
export const conciergeWorkspace = defineWorkingEnvironment({
|
|
13
|
+
options: { limits: { maxVfsBytes: 16 * 1024 * 1024 } },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function conciergeRepl(actor: string, budgetUsd: number) {
|
|
17
|
+
const session = JsSession.create({ actor });
|
|
18
|
+
session.register(defineFn({
|
|
19
|
+
name: "trip__budget",
|
|
20
|
+
description: "Read the traveller's remaining budget in USD",
|
|
21
|
+
input: z.object({}),
|
|
22
|
+
readOnlyHint: true,
|
|
23
|
+
handler: () => ({ budgetUsd }),
|
|
24
|
+
}));
|
|
25
|
+
return defineRepl({ language: "javascript", session });
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MemoryFoundryDataAdapter, defineApplication } from "glove-foundry";
|
|
2
|
+
import { travellerAccount, travellerInbound, travellerOutbound } from "./agents/concierge/topology.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The application is where runtime data enters: which adapter persists
|
|
6
|
+
* instances, which accounts and routes exist. `MemoryFoundryDataAdapter` keeps
|
|
7
|
+
* everything in process, so restarting `pnpm dev` starts clean. Swap it for
|
|
8
|
+
* your own adapter to persist across restarts.
|
|
9
|
+
*/
|
|
10
|
+
export const data = new MemoryFoundryDataAdapter();
|
|
11
|
+
|
|
12
|
+
export default defineApplication({
|
|
13
|
+
name: "{{projectName}}",
|
|
14
|
+
data,
|
|
15
|
+
accounts: [travellerAccount],
|
|
16
|
+
routes: [travellerInbound, travellerOutbound],
|
|
17
|
+
bindings: [],
|
|
18
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Message,
|
|
3
|
+
ModelAdapter,
|
|
4
|
+
ModelPromptResult,
|
|
5
|
+
NotifySubscribersFunction,
|
|
6
|
+
PromptRequest,
|
|
7
|
+
ToolCall,
|
|
8
|
+
} from "glove-core";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A deterministic stand-in used when OPENROUTER_API_KEY is absent, so a fresh
|
|
12
|
+
* project runs end to end — real runs, real tool calls, a real event trace in
|
|
13
|
+
* the inspector — before you have configured a provider.
|
|
14
|
+
*
|
|
15
|
+
* Delete this file and the `model()` fallback in agent.ts once you have a key.
|
|
16
|
+
*/
|
|
17
|
+
export class DemoModel implements ModelAdapter {
|
|
18
|
+
readonly name = "demo-model";
|
|
19
|
+
private systemPrompt = "";
|
|
20
|
+
|
|
21
|
+
setSystemPrompt(prompt: string): void {
|
|
22
|
+
this.systemPrompt = prompt;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async prompt(
|
|
26
|
+
request: PromptRequest,
|
|
27
|
+
notify: NotifySubscribersFunction,
|
|
28
|
+
): Promise<ModelPromptResult> {
|
|
29
|
+
const toolCompleted = request.messages.some(
|
|
30
|
+
(message) => Array.isArray(message.tool_results) && message.tool_results.length > 0,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!toolCompleted) {
|
|
34
|
+
// First turn: call a real tool so the trace shows tool_use -> result.
|
|
35
|
+
// ToolCall's fields are optional, so notify() reads the local literals.
|
|
36
|
+
const callId = "call_demo_flights";
|
|
37
|
+
const toolName = "find_flights";
|
|
38
|
+
const input = { from: "LIS", to: "NBO", maxPrice: 5_000 };
|
|
39
|
+
const call: ToolCall = { id: callId, tool_name: toolName, input_args: input };
|
|
40
|
+
await notify("tool_use", { id: callId, name: toolName, input });
|
|
41
|
+
await notify("model_response_complete", {
|
|
42
|
+
text: "",
|
|
43
|
+
tool_calls: [call],
|
|
44
|
+
stop_reason: "tool_use",
|
|
45
|
+
tokens_in: 14,
|
|
46
|
+
tokens_out: 6,
|
|
47
|
+
});
|
|
48
|
+
const message: Message = { sender: "agent", text: "", tool_calls: [call] };
|
|
49
|
+
return { messages: [message], tokens_in: 14, tokens_out: 6 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const asked = [...request.messages]
|
|
53
|
+
.reverse()
|
|
54
|
+
.find((message) => message.sender === "user" && !message.tool_results?.length)?.text;
|
|
55
|
+
const text = [
|
|
56
|
+
`Here is what I found for "${asked ?? "your trip"}".`,
|
|
57
|
+
"Three flights match. Meridian at 13:15 is the cheapest at $494; Northwind at 22:05 is the fastest at 9h.",
|
|
58
|
+
"Tell me your dates and I will check the calendar and hold them.",
|
|
59
|
+
"",
|
|
60
|
+
"(This is the built-in demo model. Set OPENROUTER_API_KEY in .env.local for real answers.)",
|
|
61
|
+
].join("\n");
|
|
62
|
+
await notify("text_delta", { text });
|
|
63
|
+
await notify("model_response_complete", {
|
|
64
|
+
text,
|
|
65
|
+
stop_reason: "end_turn",
|
|
66
|
+
tokens_in: 32,
|
|
67
|
+
tokens_out: 48,
|
|
68
|
+
});
|
|
69
|
+
void this.systemPrompt;
|
|
70
|
+
return { messages: [{ sender: "agent", text }], tokens_in: 32, tokens_out: 48 };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed client for this project's agents. `FoundryRoutes` is generated into
|
|
3
|
+
* .foundry/routes.d.ts on every `glove foundry dev`, so `agent("concierge")`
|
|
4
|
+
* is checked against the files that actually exist.
|
|
5
|
+
*
|
|
6
|
+
* Run with: npx tsx src/client.ts (while `pnpm dev` is running)
|
|
7
|
+
*/
|
|
8
|
+
import { createFoundryClient } from "glove-foundry/client";
|
|
9
|
+
import type { FoundryRoutes } from "../.foundry/routes.js";
|
|
10
|
+
|
|
11
|
+
const foundry = createFoundryClient<FoundryRoutes>({ baseUrl: "http://127.0.0.1:4141" });
|
|
12
|
+
|
|
13
|
+
const agent = await foundry.agent("concierge").create({
|
|
14
|
+
workspaceId: "demo",
|
|
15
|
+
context: { budgetUsd: 2_500 },
|
|
16
|
+
});
|
|
17
|
+
const conversation = await foundry.createConversation(agent.id);
|
|
18
|
+
const run = await foundry.send(agent.id, conversation.id, "I want to fly from Lisbon to Nairobi in April.");
|
|
19
|
+
|
|
20
|
+
console.log(`run ${run.id} -> http://127.0.0.1:4141/runs/${run.id}`);
|
|
21
|
+
const completed = await run.wait();
|
|
22
|
+
console.log(completed.output);
|