@finalai/agents 0.8.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.
Potentially problematic release.
This version of @finalai/agents might be problematic. Click here for more details.
- package/README.md +30 -0
- package/dist/modules/agent-client/acp-frames.d.mts +19 -0
- package/dist/modules/agent-client/acp-frames.mjs +49 -0
- package/dist/modules/agent-client/acp-frames.mjs.map +7 -0
- package/dist/modules/agent-client/acp.d.mts +43 -0
- package/dist/modules/agent-client/acp.mjs +76 -0
- package/dist/modules/agent-client/acp.mjs.map +7 -0
- package/dist/modules/agent-client/async-queue.d.mts +10 -0
- package/dist/modules/agent-client/async-queue.mjs +66 -0
- package/dist/modules/agent-client/async-queue.mjs.map +7 -0
- package/dist/modules/agent-client/build.d.mts +107 -0
- package/dist/modules/agent-client/build.mjs +222 -0
- package/dist/modules/agent-client/build.mjs.map +7 -0
- package/dist/modules/agent-client/client.d.mts +111 -0
- package/dist/modules/agent-client/client.mjs +198 -0
- package/dist/modules/agent-client/client.mjs.map +7 -0
- package/dist/modules/agent-client/collection-events.d.mts +35 -0
- package/dist/modules/agent-client/collection-events.mjs +142 -0
- package/dist/modules/agent-client/collection-events.mjs.map +7 -0
- package/dist/modules/agent-client/config-options.d.mts +24 -0
- package/dist/modules/agent-client/config-options.mjs +49 -0
- package/dist/modules/agent-client/config-options.mjs.map +7 -0
- package/dist/modules/agent-client/context.d.mts +31 -0
- package/dist/modules/agent-client/context.mjs +11 -0
- package/dist/modules/agent-client/context.mjs.map +7 -0
- package/dist/modules/agent-client/conversations.d.mts +385 -0
- package/dist/modules/agent-client/conversations.mjs +1156 -0
- package/dist/modules/agent-client/conversations.mjs.map +7 -0
- package/dist/modules/agent-client/errors.d.mts +85 -0
- package/dist/modules/agent-client/errors.mjs +94 -0
- package/dist/modules/agent-client/errors.mjs.map +7 -0
- package/dist/modules/agent-client/event-stream.d.mts +14 -0
- package/dist/modules/agent-client/event-stream.mjs +89 -0
- package/dist/modules/agent-client/event-stream.mjs.map +7 -0
- package/dist/modules/agent-client/frame-stream.d.mts +51 -0
- package/dist/modules/agent-client/frame-stream.mjs +333 -0
- package/dist/modules/agent-client/frame-stream.mjs.map +7 -0
- package/dist/modules/agent-client/http.d.mts +68 -0
- package/dist/modules/agent-client/http.mjs +168 -0
- package/dist/modules/agent-client/http.mjs.map +7 -0
- package/dist/modules/agent-client/index.d.mts +17 -0
- package/dist/modules/agent-client/index.mjs +86 -0
- package/dist/modules/agent-client/index.mjs.map +7 -0
- package/dist/modules/agent-client/live-stream.d.mts +18 -0
- package/dist/modules/agent-client/live-stream.mjs +94 -0
- package/dist/modules/agent-client/live-stream.mjs.map +7 -0
- package/dist/modules/agent-client/realtime.d.mts +72 -0
- package/dist/modules/agent-client/realtime.mjs +186 -0
- package/dist/modules/agent-client/realtime.mjs.map +7 -0
- package/dist/modules/agent-client/sandbox-uri.d.mts +19 -0
- package/dist/modules/agent-client/sandbox-uri.mjs +43 -0
- package/dist/modules/agent-client/sandbox-uri.mjs.map +7 -0
- package/dist/modules/agent-client/sessions.d.mts +124 -0
- package/dist/modules/agent-client/sessions.mjs +350 -0
- package/dist/modules/agent-client/sessions.mjs.map +7 -0
- package/dist/modules/agent-client/test-fakes.d.mts +40 -0
- package/dist/modules/agent-client/test-fakes.mjs +94 -0
- package/dist/modules/agent-client/test-fakes.mjs.map +7 -0
- package/dist/modules/authoring/approval.d.mts +1 -0
- package/dist/modules/authoring/approval.mjs +2 -0
- package/dist/modules/authoring/approval.mjs.map +7 -0
- package/dist/modules/authoring/channels.d.mts +1 -0
- package/dist/modules/authoring/channels.mjs +2 -0
- package/dist/modules/authoring/channels.mjs.map +7 -0
- package/dist/modules/authoring/final-channel.d.mts +1 -0
- package/dist/modules/authoring/final-channel.mjs +2 -0
- package/dist/modules/authoring/final-channel.mjs.map +7 -0
- package/dist/modules/authoring/hooks.d.mts +1 -0
- package/dist/modules/authoring/hooks.mjs +2 -0
- package/dist/modules/authoring/hooks.mjs.map +7 -0
- package/dist/modules/authoring/index.d.mts +1 -0
- package/dist/modules/authoring/index.mjs +2 -0
- package/dist/modules/authoring/index.mjs.map +7 -0
- package/dist/modules/authoring/instructions.d.mts +1 -0
- package/dist/modules/authoring/instructions.mjs +2 -0
- package/dist/modules/authoring/instructions.mjs.map +7 -0
- package/dist/modules/authoring/providers.d.mts +2 -0
- package/dist/modules/authoring/providers.mjs +1 -0
- package/dist/modules/authoring/providers.mjs.map +7 -0
- package/dist/modules/authoring/tools.d.mts +1 -0
- package/dist/modules/authoring/tools.mjs +2 -0
- package/dist/modules/authoring/tools.mjs.map +7 -0
- package/dist/modules/cli/build.d.mts +22 -0
- package/dist/modules/cli/build.mjs +77 -0
- package/dist/modules/cli/build.mjs.map +7 -0
- package/dist/modules/cli/channel-socket.d.mts +9 -0
- package/dist/modules/cli/channel-socket.mjs +114 -0
- package/dist/modules/cli/channel-socket.mjs.map +7 -0
- package/dist/modules/cli/cli.d.mts +16 -0
- package/dist/modules/cli/cli.mjs +151 -0
- package/dist/modules/cli/cli.mjs.map +7 -0
- package/dist/modules/cli/conformance.d.mts +36 -0
- package/dist/modules/cli/conformance.mjs +220 -0
- package/dist/modules/cli/conformance.mjs.map +7 -0
- package/dist/modules/cli/dev.d.mts +24 -0
- package/dist/modules/cli/dev.mjs +263 -0
- package/dist/modules/cli/dev.mjs.map +7 -0
- package/dist/modules/cli/files.d.mts +18 -0
- package/dist/modules/cli/files.mjs +66 -0
- package/dist/modules/cli/files.mjs.map +7 -0
- package/dist/modules/cli/harness-descriptions.d.mts +20 -0
- package/dist/modules/cli/harness-descriptions.mjs +70 -0
- package/dist/modules/cli/harness-descriptions.mjs.map +7 -0
- package/dist/modules/cli/main.d.mts +2 -0
- package/dist/modules/cli/main.mjs +20 -0
- package/dist/modules/cli/main.mjs.map +7 -0
- package/dist/modules/cli/output.d.mts +14 -0
- package/dist/modules/cli/output.mjs +94 -0
- package/dist/modules/cli/output.mjs.map +7 -0
- package/dist/modules/cli/publish.d.mts +14 -0
- package/dist/modules/cli/publish.mjs +89 -0
- package/dist/modules/cli/publish.mjs.map +7 -0
- package/dist/modules/compile.d.mts +3 -0
- package/dist/modules/compile.mjs +62 -0
- package/dist/modules/compile.mjs.map +7 -0
- package/dist/modules/harnesses.d.mts +1 -0
- package/dist/modules/harnesses.mjs +2 -0
- package/dist/modules/harnesses.mjs.map +7 -0
- package/dist/modules/index.browser.d.mts +4 -0
- package/dist/modules/index.browser.mjs +8 -0
- package/dist/modules/index.browser.mjs.map +7 -0
- package/dist/modules/index.d.mts +3 -0
- package/dist/modules/index.mjs +6 -0
- package/dist/modules/index.mjs.map +7 -0
- package/dist/modules/models.d.mts +1 -0
- package/dist/modules/models.mjs +2 -0
- package/dist/modules/models.mjs.map +7 -0
- package/dist/modules/public.d.mts +6 -0
- package/dist/modules/public.mjs +68 -0
- package/dist/modules/public.mjs.map +7 -0
- package/dist/modules/sandboxes.d.mts +1 -0
- package/dist/modules/sandboxes.mjs +2 -0
- package/dist/modules/sandboxes.mjs.map +7 -0
- package/examples/custom-harness/README.md +16 -0
- package/examples/custom-harness/agent.ts +154 -0
- package/examples/custom-sandbox/README.md +12 -0
- package/examples/custom-sandbox/agent.ts +14 -0
- package/examples/custom-sandbox/sandbox.ts +22 -0
- package/examples/release-scout/README.md +11 -0
- package/examples/release-scout/agent/agent.ts +22 -0
- package/examples/release-scout/agent/channels/github-signature.ts +27 -0
- package/examples/release-scout/agent/channels/github.ts +38 -0
- package/examples/release-scout/agent/connections/linear.ts +9 -0
- package/examples/release-scout/agent/hooks/audit.ts +24 -0
- package/examples/release-scout/agent/instructions/org-context.ts +26 -0
- package/examples/release-scout/agent/instructions.md +2 -0
- package/examples/release-scout/agent/sandbox.ts +20 -0
- package/examples/release-scout/agent/schedules/daily-digest.md +7 -0
- package/examples/release-scout/agent/skills/triage/SKILL.md +8 -0
- package/examples/release-scout/agent/skills/triage/checklist.md +3 -0
- package/examples/release-scout/agent/tools/lookup_release_notes.ts +24 -0
- package/examples/release-scout/client.ts +123 -0
- package/package.json +126 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
credentials,
|
|
3
|
+
defineAgent,
|
|
4
|
+
defineHarness,
|
|
5
|
+
type AuthoredHarnessEndpointIface,
|
|
6
|
+
} from '@finalai/agents';
|
|
7
|
+
import { type JsonValue, type SecretReaderIface } from '@finalai/agent-provider';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
|
|
10
|
+
const FLEET_ORIGIN = 'https://fleet.acme.dev';
|
|
11
|
+
const FLEET_SECRET = 'acme-fleet-key';
|
|
12
|
+
|
|
13
|
+
type FleetConfig = {
|
|
14
|
+
region: string;
|
|
15
|
+
apiKeySecret: typeof FLEET_SECRET;
|
|
16
|
+
credentials: JsonValue;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface FleetRequestArgsIface {
|
|
20
|
+
secrets: SecretReaderIface;
|
|
21
|
+
path: string;
|
|
22
|
+
method: 'GET' | 'POST' | 'DELETE';
|
|
23
|
+
body?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const requestFleet = async (args: FleetRequestArgsIface): Promise<string> => {
|
|
27
|
+
const token = await args.secrets.get(FLEET_SECRET);
|
|
28
|
+
if (token == null) throw new Error(`Missing secret ${FLEET_SECRET}`);
|
|
29
|
+
const response = await fetch(`${FLEET_ORIGIN}${args.path}`, {
|
|
30
|
+
method: args.method,
|
|
31
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
32
|
+
body: args.body == null ? undefined : JSON.stringify(args.body),
|
|
33
|
+
signal: AbortSignal.timeout(30_000),
|
|
34
|
+
});
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
if (!response.ok) throw new Error(`Fleet request failed (${response.status})`);
|
|
37
|
+
return text;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const fleetEndpointSchema = z.object({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
generation: z.number().int().nonnegative(),
|
|
43
|
+
wsUrl: z.url().refine((value) => {
|
|
44
|
+
const url = new URL(value);
|
|
45
|
+
return url.protocol === 'wss:' && url.host === new URL(FLEET_ORIGIN).host;
|
|
46
|
+
}),
|
|
47
|
+
token: z.string().min(1),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const endpointFrom = (text: string): AuthoredHarnessEndpointIface => {
|
|
51
|
+
const endpoint = fleetEndpointSchema.parse(JSON.parse(text));
|
|
52
|
+
return {
|
|
53
|
+
ref: { harnessSessionRef: endpoint.id, generation: endpoint.generation },
|
|
54
|
+
transport: 'ws',
|
|
55
|
+
url: endpoint.wsUrl,
|
|
56
|
+
headers: { authorization: `Bearer ${endpoint.token}` },
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const fleetHealthSchema = z.object({ ok: z.boolean(), detail: z.string().optional() });
|
|
61
|
+
|
|
62
|
+
export const acmeFleet = defineHarness<FleetConfig>({
|
|
63
|
+
describe: async () => ({
|
|
64
|
+
configSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
region: { type: 'string' },
|
|
68
|
+
apiKeySecret: { const: FLEET_SECRET },
|
|
69
|
+
credentials: { $ref: 'final://schemas/credential-policy' },
|
|
70
|
+
},
|
|
71
|
+
required: ['region', 'apiKeySecret', 'credentials'],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
acp: { transports: ['ws'], mcp: ['http'] },
|
|
75
|
+
inference: { kinds: ['harness-owned'], delivery: 'at-open' },
|
|
76
|
+
supports: {
|
|
77
|
+
observe: false,
|
|
78
|
+
resumeReplay: true,
|
|
79
|
+
fork: false,
|
|
80
|
+
authorMeta: false,
|
|
81
|
+
nativeToolPolicy: false,
|
|
82
|
+
},
|
|
83
|
+
promptWhileRunning: 'queue',
|
|
84
|
+
sessionSurvivesDisconnect: true,
|
|
85
|
+
}),
|
|
86
|
+
open: async ({ sessionId, harnessSessionId, config, mcpServers, secrets }) =>
|
|
87
|
+
endpointFrom(
|
|
88
|
+
await requestFleet({
|
|
89
|
+
secrets,
|
|
90
|
+
path: '/sessions',
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: {
|
|
93
|
+
sessionId,
|
|
94
|
+
idempotencyKey: harnessSessionId,
|
|
95
|
+
region: config.region,
|
|
96
|
+
program: 'claude-code',
|
|
97
|
+
mcpServers,
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
resume: async ({ ref, lastSeq, mcpServers, secrets }) =>
|
|
102
|
+
endpointFrom(
|
|
103
|
+
await requestFleet({
|
|
104
|
+
secrets,
|
|
105
|
+
path: `/sessions/${encodeURIComponent(ref.harnessSessionRef)}/resume`,
|
|
106
|
+
method: 'POST',
|
|
107
|
+
body: { generation: ref.generation, lastSeq, mcpServers },
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
park: async ({ ref, secrets }) => {
|
|
111
|
+
await requestFleet({
|
|
112
|
+
secrets,
|
|
113
|
+
path: `/sessions/${encodeURIComponent(ref.harnessSessionRef)}/pause`,
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: { generation: ref.generation },
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
close: async ({ ref, secrets, force }) => {
|
|
119
|
+
await requestFleet({
|
|
120
|
+
secrets,
|
|
121
|
+
path: `/sessions/${encodeURIComponent(ref.harnessSessionRef)}`,
|
|
122
|
+
method: 'DELETE',
|
|
123
|
+
body: { generation: ref.generation, force },
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
health: async ({ ref, secrets }) => {
|
|
127
|
+
try {
|
|
128
|
+
return fleetHealthSchema.parse(
|
|
129
|
+
JSON.parse(
|
|
130
|
+
await requestFleet({
|
|
131
|
+
secrets,
|
|
132
|
+
path: `/sessions/${encodeURIComponent(ref.harnessSessionRef)}/health`,
|
|
133
|
+
method: 'GET',
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
return { ok: false, detail: cause instanceof Error ? cause.message : String(cause) };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export default defineAgent({
|
|
144
|
+
name: 'Release Scout',
|
|
145
|
+
secrets: [FLEET_SECRET],
|
|
146
|
+
harness: acmeFleet({
|
|
147
|
+
region: 'eu',
|
|
148
|
+
apiKeySecret: FLEET_SECRET,
|
|
149
|
+
credentials: credentials.chain({
|
|
150
|
+
payer: 'agent-owner',
|
|
151
|
+
tryInOrder: [credentials.harness()],
|
|
152
|
+
}),
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Custom sandbox provider
|
|
2
|
+
|
|
3
|
+
This example uses `@finalai/agents` for authoring and
|
|
4
|
+
`@finalai/agent-provider/sandbox` for the sandbox implementation. Build it with:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
final-agents build ./examples/custom-sandbox
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`sandbox.ts` exports a provider and its named target. `agent.ts` binds file and shell capabilities to that target, with billing explicitly in the harness configuration. The provider exposes file and shell operations through the SDK's streaming handle RPC and MCP adapters.
|
|
11
|
+
|
|
12
|
+
The in-memory backend is a small, ephemeral reference implementation with a limited command set. Its files last only while the resumed handle is live. A persistent provider implements these same methods over its backend API: `prepare` returns the preparation artifact, `start` returns serializable backend state, and `resume` reconnects to that state. The SDK contract and conformance tools are available at `@finalai/agent-provider`, `@finalai/agent-provider/sandbox`, and `@finalai/agent-provider/conformance`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { baseTools, credentials, defineAgent } from '@finalai/agents';
|
|
2
|
+
import { serverlessPi } from '@finalai/agents/harnesses';
|
|
3
|
+
import { scratch } from './sandbox';
|
|
4
|
+
|
|
5
|
+
export default defineAgent({
|
|
6
|
+
name: 'Scratch agent',
|
|
7
|
+
harness: serverlessPi({
|
|
8
|
+
credentials: credentials.chain({
|
|
9
|
+
payer: 'organization',
|
|
10
|
+
tryInOrder: [credentials.platform()],
|
|
11
|
+
}),
|
|
12
|
+
}),
|
|
13
|
+
tools: baseTools({ shell: scratch, files: scratch }),
|
|
14
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineSandboxProvider } from '@finalai/agents';
|
|
2
|
+
import { createMemorySandbox } from '@finalai/agent-provider/sandbox';
|
|
3
|
+
|
|
4
|
+
// An ephemeral reference provider. Persistent providers reconnect to their backend in resume.
|
|
5
|
+
const handle = () => ({
|
|
6
|
+
sandbox: createMemorySandbox({ cwd: '/workspace' }),
|
|
7
|
+
onSandboxStop: async () => {},
|
|
8
|
+
onSandboxDelete: async () => {},
|
|
9
|
+
onRuntimeShutdown: async () => {},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const scratchProvider = defineSandboxProvider({
|
|
13
|
+
name: 'scratch',
|
|
14
|
+
environment: () => ({
|
|
15
|
+
artifactKey: async () => 'scratch-v1',
|
|
16
|
+
prepare: async () => ({}),
|
|
17
|
+
start: async () => ({ handle: handle(), state: {} }),
|
|
18
|
+
resume: async () => handle(),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const scratch = scratchProvider.environment();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Release Scout client
|
|
2
|
+
|
|
3
|
+
`client.ts` exports separate operations so you choose which account mutations to run. Install `@finalai/agents` in a Node 26 project and import `connect`, `directSession`, `directConversation`, `groupConversation`, or `publish`. Create and publish Release Scout first; groups also require Deploy Bot. User participant IDs come from `client.people.all()`.
|
|
4
|
+
|
|
5
|
+
Every function accepts an abort signal. Pending requests are reported for the caller to inspect: answer a permission with `conversation.respond({ pendingId, optionId })`, choosing an actual option from the pending request, or answer an elicitation with its ACP outcome. No example approves a request automatically. Use `conversation.cancel()` to stop every agent in a room; `client.close()` stops local subscriptions and releases owned realtime connections.
|
|
6
|
+
|
|
7
|
+
Definition publication compiles the repository ref and activates its definition schedules and channels. It is an explicit operation. Use `definitions.compile()` or `definitions.testRevision()` for an unpublished snapshot. `schedules.runNow({ agent: 'release-scout', key: 'daily-digest' })` triggers an existing schedule. `client.acp.connect({ agent: 'release-scout' })` opens a native ACP client connection.
|
|
8
|
+
|
|
9
|
+
The published package includes this client and the complete authored definition under `examples/release-scout/agent/`. It declares billing in the harness credential chain and includes sandbox setup, instructions, skills, an authored tool, the Linear connection, a schedule, a hook and the GitHub webhook. Supply the declared secrets and provider credentials before publishing it.
|
|
10
|
+
|
|
11
|
+
Conversation deliveries include session and turn ids. The example reconstructs each response with `session.response(turnId)` and waits for its durable completion, so a blank or failed turn cannot leave it waiting for a missed conversation event. Read receipts again with `conversation.deliveries({ messageId })` after a reconnect or for a delivery admitted under the queue policy.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { baseTools, credentials, defineAgent } from '@finalai/agents';
|
|
2
|
+
import { serverlessPi } from '@finalai/agents/harnesses';
|
|
3
|
+
import { computer } from './sandbox';
|
|
4
|
+
|
|
5
|
+
export default defineAgent({
|
|
6
|
+
name: 'Release Scout',
|
|
7
|
+
description: 'Answers release questions, runs checks, posts the daily digest.',
|
|
8
|
+
// Read by tools/lookup_release_notes.ts and channels/github.ts; linear-token
|
|
9
|
+
// comes from connections/linear.ts.
|
|
10
|
+
secrets: ['github-token', 'github-webhook-secret'],
|
|
11
|
+
// Off unless declared; each capability is bound to a named sandbox target.
|
|
12
|
+
tools: baseTools({ shell: computer, files: computer }),
|
|
13
|
+
harness: serverlessPi({
|
|
14
|
+
model: 'gpt-6-sol',
|
|
15
|
+
reasoning: 'medium',
|
|
16
|
+
credentials: credentials.chain({
|
|
17
|
+
payer: 'organization',
|
|
18
|
+
tryInOrder: [credentials.apiKey({ owner: 'organization' }), credentials.platform()],
|
|
19
|
+
}),
|
|
20
|
+
permissionMode: 'ask',
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// A helper next to the channel (no default export, so not a channel itself).
|
|
2
|
+
export const verifyGithubSignature = async (
|
|
3
|
+
request: Request,
|
|
4
|
+
body: string,
|
|
5
|
+
secret: string | null,
|
|
6
|
+
): Promise<boolean> => {
|
|
7
|
+
const signature = request.headers.get('x-hub-signature-256');
|
|
8
|
+
if (
|
|
9
|
+
secret == null ||
|
|
10
|
+
secret.length === 0 ||
|
|
11
|
+
signature == null ||
|
|
12
|
+
!/^sha256=[a-f0-9]{64}$/i.test(signature)
|
|
13
|
+
)
|
|
14
|
+
return false;
|
|
15
|
+
const digest = Uint8Array.from(signature.slice(7).match(/.{2}/g) ?? [], (byte) =>
|
|
16
|
+
Number.parseInt(byte, 16),
|
|
17
|
+
);
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
const key = await crypto.subtle.importKey(
|
|
20
|
+
'raw',
|
|
21
|
+
encoder.encode(secret),
|
|
22
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
23
|
+
false,
|
|
24
|
+
['verify'],
|
|
25
|
+
);
|
|
26
|
+
return crypto.subtle.verify('HMAC', key, digest, encoder.encode(body));
|
|
27
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineChannel, POST } from '@finalai/agents/channels';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import { verifyGithubSignature } from './github-signature';
|
|
4
|
+
|
|
5
|
+
const actionSchema = z.object({ action: z.string() });
|
|
6
|
+
const releaseEventSchema = actionSchema.extend({ release: z.object({ tag_name: z.string() }) });
|
|
7
|
+
|
|
8
|
+
export default defineChannel({
|
|
9
|
+
routes: [
|
|
10
|
+
POST('/webhook', async ({ request, secrets, receive }) => {
|
|
11
|
+
const body = await request.text();
|
|
12
|
+
if (
|
|
13
|
+
!(await verifyGithubSignature(request, body, await secrets.get('github-webhook-secret')))
|
|
14
|
+
) {
|
|
15
|
+
return new Response(null, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
let raw: unknown;
|
|
18
|
+
try {
|
|
19
|
+
raw = JSON.parse(body);
|
|
20
|
+
} catch {
|
|
21
|
+
return new Response(null, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const action = actionSchema.safeParse(raw);
|
|
24
|
+
if (!action.success) return new Response(null, { status: 400 });
|
|
25
|
+
if (action.data.action !== 'published') return new Response(null, { status: 204 });
|
|
26
|
+
const parsed = releaseEventSchema.safeParse(raw);
|
|
27
|
+
if (!parsed.success) return new Response(null, { status: 400 });
|
|
28
|
+
await receive({
|
|
29
|
+
target: { kind: 'bound-conversation' },
|
|
30
|
+
content: [
|
|
31
|
+
{ type: 'text', text: `GitHub release published: ${parsed.data.release.tag_name}` },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
return new Response(null, { status: 202 });
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
turnPolicy: 'steer',
|
|
38
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineMcpClientConnection } from '@finalai/agents';
|
|
2
|
+
import { always } from '@finalai/agents/approval';
|
|
3
|
+
|
|
4
|
+
export default defineMcpClientConnection({
|
|
5
|
+
url: 'https://mcp.linear.app/mcp',
|
|
6
|
+
// Bearer from the secrets manager; the secret never reaches the harness.
|
|
7
|
+
auth: { secretKey: 'linear-token', headerName: null },
|
|
8
|
+
tools: { allow: ['search_issues', 'create_issue'], approval: { create_issue: always() } },
|
|
9
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineHook } from '@finalai/agents/hooks';
|
|
2
|
+
import { toolResultFrom } from '@finalai/agents/tools';
|
|
3
|
+
import lookupReleaseNotes from '../tools/lookup_release_notes';
|
|
4
|
+
|
|
5
|
+
export default defineHook({
|
|
6
|
+
events: {
|
|
7
|
+
async 'turn.ended'(event, ctx) {
|
|
8
|
+
if (event.data.stopReason === 'error') {
|
|
9
|
+
await ctx.final.post({ text: 'Something went wrong on my side; I have logged it.' });
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
async tool_call_update(event) {
|
|
13
|
+
const notes = toolResultFrom(event, lookupReleaseNotes);
|
|
14
|
+
if (notes?.found === false)
|
|
15
|
+
console.warn('release notes missing', { tag: event.data.rawInput['tag'] });
|
|
16
|
+
},
|
|
17
|
+
async '*'(event, ctx) {
|
|
18
|
+
await fetch('https://audit.internal/events', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
body: JSON.stringify({ id: event.meta.id, session: ctx.session.id, type: event.type }),
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineDynamic } from '@finalai/agents';
|
|
2
|
+
import { defineInstructions } from '@finalai/agents/instructions';
|
|
3
|
+
import * as z from 'zod';
|
|
4
|
+
|
|
5
|
+
const calendarSchema = z.object({ nextRelease: z.string(), onCall: z.string() });
|
|
6
|
+
|
|
7
|
+
export default defineDynamic({
|
|
8
|
+
// Only needed because the value is unknown until a session starts and Build
|
|
9
|
+
// still has to show something in this fragment's slot.
|
|
10
|
+
description: "Adds the organization's release calendar and current on-call.",
|
|
11
|
+
preview: 'Release calendar: 2.4.1 on 2026-10-10. On-call: @mina.',
|
|
12
|
+
events: {
|
|
13
|
+
'session.started': async (_event, ctx) => {
|
|
14
|
+
const url = new URL('https://ops.internal/calendar');
|
|
15
|
+
url.searchParams.set('org', ctx.session.organizationId);
|
|
16
|
+
const res = await fetch(url);
|
|
17
|
+
const text = await res.text();
|
|
18
|
+
if (!res.ok) throw new Error(`Release calendar request failed (${res.status})`);
|
|
19
|
+
const { nextRelease, onCall } = calendarSchema.parse(JSON.parse(text));
|
|
20
|
+
return defineInstructions({
|
|
21
|
+
role: 'user',
|
|
22
|
+
content: `Release calendar: ${nextRelease}. On-call: ${onCall}.`,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineSandbox } from '@finalai/agents';
|
|
2
|
+
import { computerSetupGuide } from '@finalai/agents/sandboxes';
|
|
3
|
+
|
|
4
|
+
// A saved guide by reference. Everything after
|
|
5
|
+
// ref/version overrides the saved version's defaults.
|
|
6
|
+
export const computer = computerSetupGuide({
|
|
7
|
+
ref: 'release-scout-dev',
|
|
8
|
+
version: 'latest',
|
|
9
|
+
repositories: [{ alias: 'app', repository: 'final/app', ref: 'main' }],
|
|
10
|
+
variables: ['GITHUB_TOKEN', 'NPM_TOKEN'],
|
|
11
|
+
workspace: 'session',
|
|
12
|
+
networkPolicy: 'allow-all',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// The default export opens the target named `computer` for each session.
|
|
16
|
+
export default defineSandbox(async ({ session }) => {
|
|
17
|
+
const sandbox = await computer.open({ access: 'write' });
|
|
18
|
+
await sandbox.writeTextFile({ path: '.final/session', content: session.id });
|
|
19
|
+
return sandbox;
|
|
20
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: triage
|
|
3
|
+
description: Triage a failing release check into a Linear issue with owner and severity.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
1. Reproduce the failure in the computer (`shell`) before filing anything.
|
|
7
|
+
2. Search Linear for an existing issue with `search_issues`.
|
|
8
|
+
3. File with `create_issue` only when nothing matches; include the release tag and the failing command.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineTool } from '@finalai/agents';
|
|
2
|
+
import { never } from '@finalai/agents/approval';
|
|
3
|
+
import * as z from 'zod';
|
|
4
|
+
|
|
5
|
+
const releaseSchema = z.object({ body: z.string().nullable(), published_at: z.string() });
|
|
6
|
+
|
|
7
|
+
export default defineTool({
|
|
8
|
+
description: 'Fetch the release notes for a version tag.',
|
|
9
|
+
inputSchema: z.object({ tag: z.string().regex(/^\d+\.\d+\.\d+$/) }),
|
|
10
|
+
approval: never(),
|
|
11
|
+
async execute({ tag }, ctx) {
|
|
12
|
+
const token = await ctx.secrets.get('github-token');
|
|
13
|
+
if (token == null) throw new Error('Missing secret github-token');
|
|
14
|
+
const res = await fetch(`https://api.github.com/repos/final/app/releases/tags/v${tag}`, {
|
|
15
|
+
headers: { authorization: `Bearer ${token}` },
|
|
16
|
+
signal: ctx.abortSignal,
|
|
17
|
+
});
|
|
18
|
+
const text = await res.text();
|
|
19
|
+
if (res.status === 404) return { found: false };
|
|
20
|
+
if (!res.ok) throw new Error(`Release notes request failed (${res.status})`);
|
|
21
|
+
const { body, published_at } = releaseSchema.parse(JSON.parse(text));
|
|
22
|
+
return { found: true, publishedAt: published_at, notes: body ?? '' };
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClient,
|
|
3
|
+
sessionUpdateOf,
|
|
4
|
+
SystemParticipantKind,
|
|
5
|
+
textOf,
|
|
6
|
+
type AgentClientIface,
|
|
7
|
+
} from '@finalai/agents/client';
|
|
8
|
+
import 'zod/compile';
|
|
9
|
+
|
|
10
|
+
export const directSession = async (client: AgentClientIface, signal?: AbortSignal) => {
|
|
11
|
+
const { handle, response } = await client.sessions.create({
|
|
12
|
+
agent: 'release-scout',
|
|
13
|
+
message: 'Did 2.4.1 ship?',
|
|
14
|
+
signal,
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
if (response == null) throw new Error('The initial message must create a response');
|
|
18
|
+
for await (const event of response.stream({ signal })) {
|
|
19
|
+
const update = sessionUpdateOf(event);
|
|
20
|
+
if (update?.sessionUpdate === 'agent_message_chunk') process.stdout.write(textOf(update));
|
|
21
|
+
if (event.pendingId != null) console.log('Awaiting a decision:', event.pendingId);
|
|
22
|
+
}
|
|
23
|
+
console.log((await response.result({ signal })).stopReason);
|
|
24
|
+
await handle.observe('The release was moved to Friday.', { signal });
|
|
25
|
+
return await handle.manifest({ signal });
|
|
26
|
+
} finally {
|
|
27
|
+
// Close cleanup has its own lifetime, even when the prompt was aborted.
|
|
28
|
+
await handle.close();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// The source must be idle on a harness with native forks, such as remote Claude Code.
|
|
33
|
+
export const forkNativeSession = (
|
|
34
|
+
client: AgentClientIface,
|
|
35
|
+
args: {
|
|
36
|
+
sourceSessionId: string;
|
|
37
|
+
idempotencyKey: string;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
},
|
|
40
|
+
) =>
|
|
41
|
+
client.sessions.attach(args.sourceSessionId).fork({
|
|
42
|
+
idempotencyKey: args.idempotencyKey,
|
|
43
|
+
title: 'Alternative investigation',
|
|
44
|
+
signal: args.signal,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const directConversation = async (client: AgentClientIface, signal?: AbortSignal) => {
|
|
48
|
+
const direct = await client.conversations.direct({ agent: 'release-scout', signal });
|
|
49
|
+
const sent = await direct.send('Did 2.4.1 ship?', { signal });
|
|
50
|
+
// Durable delivery receipts also cover blank replies and a turn completed before subscribe.
|
|
51
|
+
for (const delivery of sent.deliveries) {
|
|
52
|
+
if (delivery.sessionId == null || delivery.turnId == null) continue;
|
|
53
|
+
const session = client.sessions.attach(delivery.sessionId);
|
|
54
|
+
const response = await session.response(delivery.turnId, { signal });
|
|
55
|
+
for await (const event of response.stream({ signal })) {
|
|
56
|
+
const update = sessionUpdateOf(event);
|
|
57
|
+
if (update?.sessionUpdate === 'agent_message_chunk') process.stdout.write(textOf(update));
|
|
58
|
+
if (event.pendingId != null) console.log('Decision required:', event.pendingId);
|
|
59
|
+
}
|
|
60
|
+
console.log((await response.result({ signal })).stopReason);
|
|
61
|
+
}
|
|
62
|
+
const page = await direct.messages({ limit: 50, signal });
|
|
63
|
+
const last = page.messages.at(-1);
|
|
64
|
+
if (last != null) await direct.markRead({ throughSeq: last.seq }, { signal });
|
|
65
|
+
return direct;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const groupConversation = async (
|
|
69
|
+
client: AgentClientIface,
|
|
70
|
+
userId: string,
|
|
71
|
+
signal?: AbortSignal,
|
|
72
|
+
) => {
|
|
73
|
+
const group = await client.conversations.create({
|
|
74
|
+
kind: 'group',
|
|
75
|
+
title: '#releases',
|
|
76
|
+
signal,
|
|
77
|
+
participants: [{ user: userId }, { agent: 'release-scout', routingMode: 'default' }],
|
|
78
|
+
});
|
|
79
|
+
const bot = await group.addParticipant(
|
|
80
|
+
{ agent: 'deploy-bot', routingMode: 'mention' },
|
|
81
|
+
{ signal },
|
|
82
|
+
);
|
|
83
|
+
await group.send('Go ahead with the release.', { mentions: [bot.id], signal });
|
|
84
|
+
return group;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const publish = async (client: AgentClientIface, signal?: AbortSignal) => {
|
|
88
|
+
const report = await client.definitions.compatibility({
|
|
89
|
+
definition: 'release-scout',
|
|
90
|
+
harness: '@finalai/final-agent-provider/harnesses/serverless-pi',
|
|
91
|
+
signal,
|
|
92
|
+
});
|
|
93
|
+
const revision = await client.definitions.publish({
|
|
94
|
+
definition: 'release-scout',
|
|
95
|
+
ref: 'main',
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
return { report, revision, guides: await client.computerSetupGuides.list({ signal }) };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const connect = (args: { host: string; apiKey: string }) =>
|
|
102
|
+
createClient({
|
|
103
|
+
host: args.host,
|
|
104
|
+
auth: { apiKey: args.apiKey },
|
|
105
|
+
headers: () => ({ 'x-request-source': 'release-scout-example' }),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Called by an external job after its work finishes; repeat the same event id on retry.
|
|
109
|
+
export const postBuildResult = async (args: {
|
|
110
|
+
client: AgentClientIface;
|
|
111
|
+
conversationId: string;
|
|
112
|
+
runId: string;
|
|
113
|
+
text: string;
|
|
114
|
+
signal?: AbortSignal;
|
|
115
|
+
}) =>
|
|
116
|
+
args.client.conversations.get(args.conversationId).postEvent(
|
|
117
|
+
{
|
|
118
|
+
source: { kind: SystemParticipantKind.Tool, id: 'release-build' },
|
|
119
|
+
eventId: `${args.runId}:completed`,
|
|
120
|
+
text: args.text,
|
|
121
|
+
},
|
|
122
|
+
{ signal: args.signal },
|
|
123
|
+
);
|