@positronic/spec 0.0.37 → 0.0.39
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/api.d.ts +47 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +470 -1
- package/dist/api.js.map +1 -1
- package/dist/src/api.js +975 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -55,6 +55,19 @@ export declare const brains: {
|
|
|
55
55
|
* Test GET /brains - List all brains
|
|
56
56
|
*/
|
|
57
57
|
list(fetch: Fetch): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Test GET /brains?q=<query> - Search brains by query string
|
|
60
|
+
* Returns brains matching the query (by title, filename, or description).
|
|
61
|
+
* The matching algorithm is implementation-defined; the spec only verifies
|
|
62
|
+
* the response structure and that results are relevant to the query.
|
|
63
|
+
*/
|
|
64
|
+
search(fetch: Fetch, query: string): Promise<{
|
|
65
|
+
brains: Array<{
|
|
66
|
+
title: string;
|
|
67
|
+
description: string;
|
|
68
|
+
}>;
|
|
69
|
+
count: number;
|
|
70
|
+
} | null>;
|
|
58
71
|
/**
|
|
59
72
|
* Test GET /brains/:identifier - Get brain structure/definition
|
|
60
73
|
* (For future brain exploration/info command)
|
|
@@ -89,6 +102,40 @@ export declare const brains: {
|
|
|
89
102
|
* Test DELETE /brains/runs/:runId - Kill/cancel a running brain run
|
|
90
103
|
*/
|
|
91
104
|
kill(fetch: Fetch, runId: string): Promise<boolean>;
|
|
105
|
+
/**
|
|
106
|
+
* Test DELETE /brains/runs/:runId for a brain suspended on a webhook.
|
|
107
|
+
* This tests that killing a webhook-suspended brain:
|
|
108
|
+
* 1. Returns 204 (not 409)
|
|
109
|
+
* 2. Updates status to CANCELLED
|
|
110
|
+
* 3. Clears webhook registrations (webhook no longer resumes the brain)
|
|
111
|
+
*
|
|
112
|
+
* Requires a brain with a loop step that will pause on a webhook.
|
|
113
|
+
*/
|
|
114
|
+
killSuspended(fetch: Fetch, loopBrainIdentifier: string, webhookSlug: string, webhookPayload: Record<string, any>): Promise<boolean>;
|
|
115
|
+
/**
|
|
116
|
+
* Test that loop steps emit proper LOOP_* events in the SSE stream.
|
|
117
|
+
* Requires a brain with a loop step that will pause on a webhook.
|
|
118
|
+
*
|
|
119
|
+
* Expected events before webhook pause:
|
|
120
|
+
* - LOOP_START (with prompt and optional system)
|
|
121
|
+
* - LOOP_ITERATION
|
|
122
|
+
* - LOOP_TOOL_CALL
|
|
123
|
+
* - LOOP_WEBHOOK (before WEBHOOK event)
|
|
124
|
+
* - WEBHOOK
|
|
125
|
+
*/
|
|
126
|
+
watchLoopEvents(fetch: Fetch, loopBrainIdentifier: string): Promise<boolean>;
|
|
127
|
+
/**
|
|
128
|
+
* Test full loop webhook resumption flow:
|
|
129
|
+
* 1. Start a loop brain that will pause on a webhook
|
|
130
|
+
* 2. Verify it pauses with WEBHOOK event
|
|
131
|
+
* 3. Trigger the webhook with a response
|
|
132
|
+
* 4. Verify the brain resumes and emits WEBHOOK_RESPONSE and LOOP_TOOL_RESULT
|
|
133
|
+
*
|
|
134
|
+
* Requires:
|
|
135
|
+
* - A brain with a loop step that calls a tool returning { waitFor: webhook(...) }
|
|
136
|
+
* - The webhook slug and identifier to trigger
|
|
137
|
+
*/
|
|
138
|
+
loopWebhookResume(fetch: Fetch, loopBrainIdentifier: string, webhookSlug: string, webhookPayload: Record<string, any>): Promise<boolean>;
|
|
92
139
|
};
|
|
93
140
|
export declare const schedules: {
|
|
94
141
|
/**
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,KAAK,KAAK,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAErD,wBAAsB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAyB/D;AAED,eAAO,MAAM,SAAS;IACpB;;OAEG;gBACe,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IAyE1C;;OAEG;kBACiB,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IAwE5C;;OAEG;kBACiB,KAAK,OAAO,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAyBzD;;OAEG;qBACoB,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IA0C/C;;OAEG;mCACkC,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IA6F7D;;OAEG;iCACgC,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;CA2E5D,CAAC;AAEF,eAAO,MAAM,MAAM;IACjB;;OAEG;eACc,KAAK,cAAc,MAAM,YAAY,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAwCrG;;OAEG;0BAEM,KAAK,cACA,MAAM,WACT,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAmCzB;;OAEG;uBAEM,KAAK,yBACW,MAAM,GAC5B,OAAO,CAAC,OAAO,CAAC;IAuDnB;;OAEG;iBACgB,KAAK,SAAS,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2D1D;;OAEG;mBAEM,KAAK,cACA,MAAM,UACV,MAAM,GACb,OAAO,CAAC,OAAO,CAAC;IA2DnB;;OAEG;oBACmB,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IAsD9C;;OAEG;gBACe,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IA6D1C;;;OAGG;wBACuB,KAAK,cAAc,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqFtE;;OAEG;sBACqB,KAAK,cAAc,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAsEpE;;OAEG;kBACiB,KAAK,SAAS,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA0F3D;;OAEG;0BACyB,KAAK,oBAAoB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B9E;;;OAGG;iCAEM,KAAK,uBACS,MAAM,GAC1B,OAAO,CAAC,OAAO,CAAC;IAkDnB;;OAEG;wBAEM,KAAK,uBACS,MAAM,GAC1B,OAAO,CAAC,OAAO,CAAC;IA+DnB;;OAEG;iBAEM,KAAK,cACA,MAAM,UACV,MAAM,aACH,MAAM,eACJ,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAuCzB;;OAEG;gBACe,KAAK,SAAS,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { STATUS } from '@positronic/core';
|
|
1
|
+
import { STATUS, BRAIN_EVENTS } from '@positronic/core';
|
|
2
2
|
export async function testStatus(fetch) {
|
|
3
3
|
try {
|
|
4
4
|
const request = new Request('http://example.com/status', {
|
|
@@ -570,6 +570,56 @@ export const brains = {
|
|
|
570
570
|
return false;
|
|
571
571
|
}
|
|
572
572
|
},
|
|
573
|
+
/**
|
|
574
|
+
* Test GET /brains?q=<query> - Search brains by query string
|
|
575
|
+
* Returns brains matching the query (by title, filename, or description).
|
|
576
|
+
* The matching algorithm is implementation-defined; the spec only verifies
|
|
577
|
+
* the response structure and that results are relevant to the query.
|
|
578
|
+
*/
|
|
579
|
+
async search(fetch, query) {
|
|
580
|
+
try {
|
|
581
|
+
const url = new URL('http://example.com/brains');
|
|
582
|
+
url.searchParams.set('q', query);
|
|
583
|
+
const request = new Request(url.toString(), {
|
|
584
|
+
method: 'GET',
|
|
585
|
+
});
|
|
586
|
+
const response = await fetch(request);
|
|
587
|
+
if (!response.ok) {
|
|
588
|
+
console.error(`GET /brains?q=${query} returned ${response.status}`);
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
const data = await response.json();
|
|
592
|
+
// Validate response structure
|
|
593
|
+
if (!Array.isArray(data.brains)) {
|
|
594
|
+
console.error(`Expected brains to be an array, got ${typeof data.brains}`);
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
if (typeof data.count !== 'number') {
|
|
598
|
+
console.error(`Expected count to be number, got ${typeof data.count}`);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
// Validate each brain has required fields
|
|
602
|
+
for (const brain of data.brains) {
|
|
603
|
+
if (!brain.title ||
|
|
604
|
+
typeof brain.title !== 'string' ||
|
|
605
|
+
!brain.description ||
|
|
606
|
+
typeof brain.description !== 'string') {
|
|
607
|
+
console.error(`Brain missing required fields or has invalid types: ${JSON.stringify(brain)}`);
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Count should match array length
|
|
612
|
+
if (data.count !== data.brains.length) {
|
|
613
|
+
console.error(`Count (${data.count}) does not match brains array length (${data.brains.length})`);
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
return data;
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
console.error(`Failed to test GET /brains?q=${query}:`, error);
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
},
|
|
573
623
|
/**
|
|
574
624
|
* Test GET /brains/:identifier - Get brain structure/definition
|
|
575
625
|
* (For future brain exploration/info command)
|
|
@@ -867,6 +917,425 @@ export const brains = {
|
|
|
867
917
|
return false;
|
|
868
918
|
}
|
|
869
919
|
},
|
|
920
|
+
/**
|
|
921
|
+
* Test DELETE /brains/runs/:runId for a brain suspended on a webhook.
|
|
922
|
+
* This tests that killing a webhook-suspended brain:
|
|
923
|
+
* 1. Returns 204 (not 409)
|
|
924
|
+
* 2. Updates status to CANCELLED
|
|
925
|
+
* 3. Clears webhook registrations (webhook no longer resumes the brain)
|
|
926
|
+
*
|
|
927
|
+
* Requires a brain with a loop step that will pause on a webhook.
|
|
928
|
+
*/
|
|
929
|
+
async killSuspended(fetch, loopBrainIdentifier, webhookSlug, webhookPayload) {
|
|
930
|
+
try {
|
|
931
|
+
// Step 1: Start the loop brain
|
|
932
|
+
const runRequest = new Request('http://example.com/brains/runs', {
|
|
933
|
+
method: 'POST',
|
|
934
|
+
headers: { 'Content-Type': 'application/json' },
|
|
935
|
+
body: JSON.stringify({ identifier: loopBrainIdentifier }),
|
|
936
|
+
});
|
|
937
|
+
const runResponse = await fetch(runRequest);
|
|
938
|
+
if (runResponse.status !== 201) {
|
|
939
|
+
console.error(`POST /brains/runs returned ${runResponse.status}, expected 201`);
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
const { brainRunId } = (await runResponse.json());
|
|
943
|
+
// Step 2: Watch until WEBHOOK event (brain pauses)
|
|
944
|
+
const watchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
|
|
945
|
+
const watchResponse = await fetch(watchRequest);
|
|
946
|
+
if (!watchResponse.ok) {
|
|
947
|
+
console.error(`GET /brains/runs/${brainRunId}/watch returned ${watchResponse.status}`);
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
let foundWebhookEvent = false;
|
|
951
|
+
if (watchResponse.body) {
|
|
952
|
+
const reader = watchResponse.body.getReader();
|
|
953
|
+
const decoder = new TextDecoder();
|
|
954
|
+
let buffer = '';
|
|
955
|
+
try {
|
|
956
|
+
while (!foundWebhookEvent) {
|
|
957
|
+
const { value, done } = await reader.read();
|
|
958
|
+
if (done)
|
|
959
|
+
break;
|
|
960
|
+
buffer += decoder.decode(value, { stream: true });
|
|
961
|
+
let eventEndIndex;
|
|
962
|
+
while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
|
|
963
|
+
const message = buffer.substring(0, eventEndIndex);
|
|
964
|
+
buffer = buffer.substring(eventEndIndex + 2);
|
|
965
|
+
if (message.startsWith('data: ')) {
|
|
966
|
+
try {
|
|
967
|
+
const event = JSON.parse(message.substring(6));
|
|
968
|
+
if (event.type === BRAIN_EVENTS.WEBHOOK) {
|
|
969
|
+
foundWebhookEvent = true;
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
if (event.type === BRAIN_EVENTS.COMPLETE ||
|
|
973
|
+
event.type === BRAIN_EVENTS.ERROR) {
|
|
974
|
+
console.error(`Brain completed/errored before WEBHOOK event: ${event.type}`);
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
catch (e) {
|
|
979
|
+
// Ignore parse errors
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
finally {
|
|
986
|
+
await reader.cancel();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (!foundWebhookEvent) {
|
|
990
|
+
console.error('Brain did not emit WEBHOOK event');
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
// Step 3: Kill the suspended brain
|
|
994
|
+
const killRequest = new Request(`http://example.com/brains/runs/${brainRunId}`, { method: 'DELETE' });
|
|
995
|
+
const killResponse = await fetch(killRequest);
|
|
996
|
+
if (killResponse.status !== 204) {
|
|
997
|
+
console.error(`DELETE /brains/runs/${brainRunId} returned ${killResponse.status}, expected 204`);
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
// Step 4: Verify status is CANCELLED via getRun
|
|
1001
|
+
const getRunRequest = new Request(`http://example.com/brains/runs/${brainRunId}`, { method: 'GET' });
|
|
1002
|
+
const getRunResponse = await fetch(getRunRequest);
|
|
1003
|
+
if (!getRunResponse.ok) {
|
|
1004
|
+
console.error(`GET /brains/runs/${brainRunId} returned ${getRunResponse.status}`);
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
const runData = (await getRunResponse.json());
|
|
1008
|
+
if (runData.status !== STATUS.CANCELLED) {
|
|
1009
|
+
console.error(`Expected status to be '${STATUS.CANCELLED}', got '${runData.status}'`);
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
// Step 5: Verify webhook no longer resumes the brain
|
|
1013
|
+
// Send a webhook - it should return 'no-match' since registrations were cleared
|
|
1014
|
+
const webhookRequest = new Request(`http://example.com/webhooks/${encodeURIComponent(webhookSlug)}`, {
|
|
1015
|
+
method: 'POST',
|
|
1016
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1017
|
+
body: JSON.stringify(webhookPayload),
|
|
1018
|
+
});
|
|
1019
|
+
const webhookResponse = await fetch(webhookRequest);
|
|
1020
|
+
// Accept 200/202 - the important thing is it doesn't resume the brain
|
|
1021
|
+
if (!webhookResponse.ok) {
|
|
1022
|
+
console.error(`POST /webhooks/${webhookSlug} returned ${webhookResponse.status}`);
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
const webhookResult = (await webhookResponse.json());
|
|
1026
|
+
// The action should be 'no-match' since webhook registrations were cleared
|
|
1027
|
+
if (webhookResult.action === 'resumed') {
|
|
1028
|
+
console.error('Webhook resumed the brain after it was killed - webhook registrations were not cleared');
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
// Verify the brain is still CANCELLED (didn't restart)
|
|
1032
|
+
const finalCheckRequest = new Request(`http://example.com/brains/runs/${brainRunId}`, { method: 'GET' });
|
|
1033
|
+
const finalCheckResponse = await fetch(finalCheckRequest);
|
|
1034
|
+
if (!finalCheckResponse.ok) {
|
|
1035
|
+
console.error(`Final GET /brains/runs/${brainRunId} returned ${finalCheckResponse.status}`);
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
const finalRunData = (await finalCheckResponse.json());
|
|
1039
|
+
if (finalRunData.status !== STATUS.CANCELLED) {
|
|
1040
|
+
console.error(`Final status check: expected '${STATUS.CANCELLED}', got '${finalRunData.status}'`);
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
console.error(`Failed to test kill suspended brain for ${loopBrainIdentifier}:`, error);
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
/**
|
|
1051
|
+
* Test that loop steps emit proper LOOP_* events in the SSE stream.
|
|
1052
|
+
* Requires a brain with a loop step that will pause on a webhook.
|
|
1053
|
+
*
|
|
1054
|
+
* Expected events before webhook pause:
|
|
1055
|
+
* - LOOP_START (with prompt and optional system)
|
|
1056
|
+
* - LOOP_ITERATION
|
|
1057
|
+
* - LOOP_TOOL_CALL
|
|
1058
|
+
* - LOOP_WEBHOOK (before WEBHOOK event)
|
|
1059
|
+
* - WEBHOOK
|
|
1060
|
+
*/
|
|
1061
|
+
async watchLoopEvents(fetch, loopBrainIdentifier) {
|
|
1062
|
+
try {
|
|
1063
|
+
// Start the loop brain
|
|
1064
|
+
const runRequest = new Request('http://example.com/brains/runs', {
|
|
1065
|
+
method: 'POST',
|
|
1066
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1067
|
+
body: JSON.stringify({ identifier: loopBrainIdentifier }),
|
|
1068
|
+
});
|
|
1069
|
+
const runResponse = await fetch(runRequest);
|
|
1070
|
+
if (runResponse.status !== 201) {
|
|
1071
|
+
console.error(`POST /brains/runs returned ${runResponse.status}, expected 201`);
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
const { brainRunId } = (await runResponse.json());
|
|
1075
|
+
// Watch the brain run
|
|
1076
|
+
const watchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
|
|
1077
|
+
const watchResponse = await fetch(watchRequest);
|
|
1078
|
+
if (!watchResponse.ok) {
|
|
1079
|
+
console.error(`GET /brains/runs/${brainRunId}/watch returned ${watchResponse.status}`);
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
// Read SSE events until we get WEBHOOK or COMPLETE/ERROR
|
|
1083
|
+
const events = [];
|
|
1084
|
+
if (watchResponse.body) {
|
|
1085
|
+
const reader = watchResponse.body.getReader();
|
|
1086
|
+
const decoder = new TextDecoder();
|
|
1087
|
+
let buffer = '';
|
|
1088
|
+
let done = false;
|
|
1089
|
+
try {
|
|
1090
|
+
while (!done) {
|
|
1091
|
+
const { value, done: streamDone } = await reader.read();
|
|
1092
|
+
if (streamDone)
|
|
1093
|
+
break;
|
|
1094
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1095
|
+
// Process complete SSE messages
|
|
1096
|
+
let eventEndIndex;
|
|
1097
|
+
while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
|
|
1098
|
+
const message = buffer.substring(0, eventEndIndex);
|
|
1099
|
+
buffer = buffer.substring(eventEndIndex + 2);
|
|
1100
|
+
if (message.startsWith('data: ')) {
|
|
1101
|
+
try {
|
|
1102
|
+
const event = JSON.parse(message.substring(6));
|
|
1103
|
+
events.push(event);
|
|
1104
|
+
// Stop on terminal events
|
|
1105
|
+
if (event.type === BRAIN_EVENTS.WEBHOOK ||
|
|
1106
|
+
event.type === BRAIN_EVENTS.COMPLETE ||
|
|
1107
|
+
event.type === BRAIN_EVENTS.ERROR) {
|
|
1108
|
+
done = true;
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
catch (e) {
|
|
1113
|
+
// Ignore parse errors
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
finally {
|
|
1120
|
+
await reader.cancel();
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
// Verify required loop events are present
|
|
1124
|
+
const hasLoopStart = events.some((e) => e.type === BRAIN_EVENTS.LOOP_START);
|
|
1125
|
+
if (!hasLoopStart) {
|
|
1126
|
+
console.error('Missing LOOP_START event in SSE stream');
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
// Verify LOOP_START has prompt field
|
|
1130
|
+
const loopStartEvent = events.find((e) => e.type === BRAIN_EVENTS.LOOP_START);
|
|
1131
|
+
if (!loopStartEvent.prompt || typeof loopStartEvent.prompt !== 'string') {
|
|
1132
|
+
console.error('LOOP_START event missing prompt field');
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
const hasLoopIteration = events.some((e) => e.type === BRAIN_EVENTS.LOOP_ITERATION);
|
|
1136
|
+
if (!hasLoopIteration) {
|
|
1137
|
+
console.error('Missing LOOP_ITERATION event in SSE stream');
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
const hasLoopToolCall = events.some((e) => e.type === BRAIN_EVENTS.LOOP_TOOL_CALL);
|
|
1141
|
+
if (!hasLoopToolCall) {
|
|
1142
|
+
console.error('Missing LOOP_TOOL_CALL event in SSE stream');
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
// If we got a WEBHOOK event, verify LOOP_WEBHOOK came before it
|
|
1146
|
+
const webhookIndex = events.findIndex((e) => e.type === BRAIN_EVENTS.WEBHOOK);
|
|
1147
|
+
if (webhookIndex !== -1) {
|
|
1148
|
+
const loopWebhookIndex = events.findIndex((e) => e.type === BRAIN_EVENTS.LOOP_WEBHOOK);
|
|
1149
|
+
if (loopWebhookIndex === -1) {
|
|
1150
|
+
console.error('Missing LOOP_WEBHOOK event before WEBHOOK event');
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
if (loopWebhookIndex >= webhookIndex) {
|
|
1154
|
+
console.error('LOOP_WEBHOOK event must come before WEBHOOK event');
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
// Verify LOOP_WEBHOOK has required fields
|
|
1158
|
+
const loopWebhookEvent = events[loopWebhookIndex];
|
|
1159
|
+
if (!loopWebhookEvent.toolCallId || !loopWebhookEvent.toolName) {
|
|
1160
|
+
console.error('LOOP_WEBHOOK event missing toolCallId or toolName fields');
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
console.error(`Failed to test loop events for ${loopBrainIdentifier}:`, error);
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
/**
|
|
1172
|
+
* Test full loop webhook resumption flow:
|
|
1173
|
+
* 1. Start a loop brain that will pause on a webhook
|
|
1174
|
+
* 2. Verify it pauses with WEBHOOK event
|
|
1175
|
+
* 3. Trigger the webhook with a response
|
|
1176
|
+
* 4. Verify the brain resumes and emits WEBHOOK_RESPONSE and LOOP_TOOL_RESULT
|
|
1177
|
+
*
|
|
1178
|
+
* Requires:
|
|
1179
|
+
* - A brain with a loop step that calls a tool returning { waitFor: webhook(...) }
|
|
1180
|
+
* - The webhook slug and identifier to trigger
|
|
1181
|
+
*/
|
|
1182
|
+
async loopWebhookResume(fetch, loopBrainIdentifier, webhookSlug, webhookPayload) {
|
|
1183
|
+
try {
|
|
1184
|
+
// Step 1: Start the loop brain
|
|
1185
|
+
const runRequest = new Request('http://example.com/brains/runs', {
|
|
1186
|
+
method: 'POST',
|
|
1187
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1188
|
+
body: JSON.stringify({ identifier: loopBrainIdentifier }),
|
|
1189
|
+
});
|
|
1190
|
+
const runResponse = await fetch(runRequest);
|
|
1191
|
+
if (runResponse.status !== 201) {
|
|
1192
|
+
console.error(`POST /brains/runs returned ${runResponse.status}, expected 201`);
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
const { brainRunId } = (await runResponse.json());
|
|
1196
|
+
// Step 2: Watch until WEBHOOK event (brain pauses)
|
|
1197
|
+
const watchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
|
|
1198
|
+
const watchResponse = await fetch(watchRequest);
|
|
1199
|
+
if (!watchResponse.ok) {
|
|
1200
|
+
console.error(`GET /brains/runs/${brainRunId}/watch returned ${watchResponse.status}`);
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
let foundWebhookEvent = false;
|
|
1204
|
+
if (watchResponse.body) {
|
|
1205
|
+
const reader = watchResponse.body.getReader();
|
|
1206
|
+
const decoder = new TextDecoder();
|
|
1207
|
+
let buffer = '';
|
|
1208
|
+
try {
|
|
1209
|
+
while (!foundWebhookEvent) {
|
|
1210
|
+
const { value, done } = await reader.read();
|
|
1211
|
+
if (done)
|
|
1212
|
+
break;
|
|
1213
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1214
|
+
let eventEndIndex;
|
|
1215
|
+
while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
|
|
1216
|
+
const message = buffer.substring(0, eventEndIndex);
|
|
1217
|
+
buffer = buffer.substring(eventEndIndex + 2);
|
|
1218
|
+
if (message.startsWith('data: ')) {
|
|
1219
|
+
try {
|
|
1220
|
+
const event = JSON.parse(message.substring(6));
|
|
1221
|
+
if (event.type === BRAIN_EVENTS.WEBHOOK) {
|
|
1222
|
+
foundWebhookEvent = true;
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
if (event.type === BRAIN_EVENTS.COMPLETE ||
|
|
1226
|
+
event.type === BRAIN_EVENTS.ERROR) {
|
|
1227
|
+
console.error(`Brain completed/errored before WEBHOOK event: ${event.type}`);
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
catch (e) {
|
|
1232
|
+
// Ignore parse errors
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
finally {
|
|
1239
|
+
await reader.cancel();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (!foundWebhookEvent) {
|
|
1243
|
+
console.error('Brain did not emit WEBHOOK event');
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
// Step 3: Trigger the webhook
|
|
1247
|
+
const webhookRequest = new Request(`http://example.com/webhooks/${encodeURIComponent(webhookSlug)}`, {
|
|
1248
|
+
method: 'POST',
|
|
1249
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1250
|
+
body: JSON.stringify(webhookPayload),
|
|
1251
|
+
});
|
|
1252
|
+
const webhookResponse = await fetch(webhookRequest);
|
|
1253
|
+
if (!webhookResponse.ok) {
|
|
1254
|
+
console.error(`POST /webhooks/${webhookSlug} returned ${webhookResponse.status}`);
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
const webhookResult = (await webhookResponse.json());
|
|
1258
|
+
if (!webhookResult.received) {
|
|
1259
|
+
console.error('Webhook was not received');
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
if (webhookResult.action !== 'resumed') {
|
|
1263
|
+
console.error(`Expected webhook action 'resumed', got '${webhookResult.action}'`);
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
// Step 4: Watch again for resumed events
|
|
1267
|
+
const resumeWatchRequest = new Request(`http://example.com/brains/runs/${brainRunId}/watch`, { method: 'GET' });
|
|
1268
|
+
const resumeWatchResponse = await fetch(resumeWatchRequest);
|
|
1269
|
+
if (!resumeWatchResponse.ok) {
|
|
1270
|
+
console.error(`GET /brains/runs/${brainRunId}/watch (resume) returned ${resumeWatchResponse.status}`);
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
const resumeEvents = [];
|
|
1274
|
+
if (resumeWatchResponse.body) {
|
|
1275
|
+
const reader = resumeWatchResponse.body.getReader();
|
|
1276
|
+
const decoder = new TextDecoder();
|
|
1277
|
+
let buffer = '';
|
|
1278
|
+
let done = false;
|
|
1279
|
+
try {
|
|
1280
|
+
while (!done) {
|
|
1281
|
+
const { value, done: streamDone } = await reader.read();
|
|
1282
|
+
if (streamDone)
|
|
1283
|
+
break;
|
|
1284
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1285
|
+
let eventEndIndex;
|
|
1286
|
+
while ((eventEndIndex = buffer.indexOf('\n\n')) !== -1) {
|
|
1287
|
+
const message = buffer.substring(0, eventEndIndex);
|
|
1288
|
+
buffer = buffer.substring(eventEndIndex + 2);
|
|
1289
|
+
if (message.startsWith('data: ')) {
|
|
1290
|
+
try {
|
|
1291
|
+
const event = JSON.parse(message.substring(6));
|
|
1292
|
+
resumeEvents.push(event);
|
|
1293
|
+
if (event.type === BRAIN_EVENTS.COMPLETE ||
|
|
1294
|
+
event.type === BRAIN_EVENTS.ERROR) {
|
|
1295
|
+
done = true;
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
catch (e) {
|
|
1300
|
+
// Ignore parse errors
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
finally {
|
|
1307
|
+
await reader.cancel();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
// Verify WEBHOOK_RESPONSE event is present
|
|
1311
|
+
const hasWebhookResponse = resumeEvents.some((e) => e.type === BRAIN_EVENTS.WEBHOOK_RESPONSE);
|
|
1312
|
+
if (!hasWebhookResponse) {
|
|
1313
|
+
console.error('Missing WEBHOOK_RESPONSE event after resume');
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
// Verify LOOP_TOOL_RESULT event is present (with the webhook response as result)
|
|
1317
|
+
const hasLoopToolResult = resumeEvents.some((e) => e.type === BRAIN_EVENTS.LOOP_TOOL_RESULT);
|
|
1318
|
+
if (!hasLoopToolResult) {
|
|
1319
|
+
console.error('Missing LOOP_TOOL_RESULT event after resume');
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
// Verify brain completed successfully
|
|
1323
|
+
const completeEvent = resumeEvents.find((e) => e.type === BRAIN_EVENTS.COMPLETE);
|
|
1324
|
+
if (!completeEvent) {
|
|
1325
|
+
console.error('Brain did not complete after resume');
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
if (completeEvent.status !== STATUS.COMPLETE) {
|
|
1329
|
+
console.error(`Expected COMPLETE status, got ${completeEvent.status}`);
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
catch (error) {
|
|
1335
|
+
console.error(`Failed to test loop webhook resume for ${loopBrainIdentifier}:`, error);
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
870
1339
|
};
|
|
871
1340
|
export const schedules = {
|
|
872
1341
|
/**
|