spine-framework 0.4.2 → 0.4.4
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/.framework/functions/_shared/agent-runner.ts +1000 -81
- package/.framework/functions/_shared/cil.ts +2 -1
- package/.framework/functions/_shared/db.ts +3 -6
- package/.framework/functions/_shared/kb.ts +61 -7
- package/.framework/functions/_shared/message-sequence.ts +53 -0
- package/.framework/functions/_shared/pipeline-runner.ts +150 -37
- package/.framework/functions/_shared/principal.ts +9 -6
- package/.framework/functions/_shared/trigger-engine.ts +149 -40
- package/.framework/functions/admin-data.ts +9 -3
- package/.framework/functions/ai-agents.ts +93 -27
- package/.framework/functions/pipeline-runner-background.ts +120 -0
- package/.framework/functions/system-cron.ts +10 -1
- package/.framework/functions/trigger-scheduler.ts +65 -5
- package/.framework/migrations/064_episode_leases.sql +196 -0
- package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
- package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
- package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
- package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
- package/.framework/schemas/spine.config.schema.json +62 -0
- package/.framework/src/App.tsx +3 -1
- package/.framework/src/components/admin/AdminSidebar.tsx +4 -2
- package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
- package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
- package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
- package/.framework/src/components/layout/Layout.tsx +4 -2
- package/.framework/src/components/layout/Sidebar.tsx +4 -2
- package/.framework/src/hooks/useBranding.ts +47 -0
- package/.framework/src/lib/branding.ts +128 -0
- package/.framework/src/pages/NoAppPage.tsx +3 -1
- package/.framework/src/pages/SetupPage.tsx +5 -2
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
- package/.framework/src/pages/auth/LoginPage.tsx +4 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
- package/.framework/templates/project.json +1 -0
- package/.framework/tests/unit/agent-runner.test.ts +17 -3
- package/.framework/tests/unit/episode-lease.test.ts +695 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
- package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
- package/.framework/tests/unit/principal-cache.test.ts +15 -0
- package/.framework/tests/unit/trigger-engine.test.ts +278 -91
- package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
- package/netlify.toml +5 -0
- package/package.json +1 -1
- package/templates/project/package-lock.json +2 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2
|
+
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* @module agent-runner
|
|
4
6
|
* @audience internal
|
|
@@ -77,6 +79,10 @@ import { createHash } from 'crypto'
|
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
80
86
|
|
|
81
87
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:buildContext:function" type="function" identity="buildContext" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
82
88
|
/**
|
|
@@ -226,6 +232,10 @@ async function buildContext(
|
|
|
226
232
|
|
|
227
233
|
|
|
228
234
|
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
229
239
|
|
|
230
240
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:DEFAULT_VECTOR_CONCURRENCY:constant" type="constant" identity="DEFAULT_VECTOR_CONCURRENCY" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
231
241
|
/**
|
|
@@ -271,6 +281,10 @@ const DEFAULT_VECTOR_CONCURRENCY = 3
|
|
|
271
281
|
|
|
272
282
|
|
|
273
283
|
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
274
288
|
|
|
275
289
|
|
|
276
290
|
|
|
@@ -320,6 +334,10 @@ const MAX_VECTOR_CONCURRENCY = 5
|
|
|
320
334
|
|
|
321
335
|
|
|
322
336
|
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
323
341
|
|
|
324
342
|
|
|
325
343
|
|
|
@@ -377,6 +395,10 @@ function resolveVectorConcurrency(value: unknown, itemCount: number): number {
|
|
|
377
395
|
|
|
378
396
|
|
|
379
397
|
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
380
402
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:mapWithConcurrency:function" type="function" identity="mapWithConcurrency" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
381
403
|
/**
|
|
382
404
|
* Maps an array of items to results using a worker function with a specified level of concurrency to efficiently process multiple asynchronous operations in parallel while managing error handling.
|
|
@@ -461,6 +483,10 @@ async function mapWithConcurrency<T, R>(
|
|
|
461
483
|
|
|
462
484
|
|
|
463
485
|
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
464
490
|
|
|
465
491
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:retrieveKnowledge:function" type="function" identity="retrieveKnowledge" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
466
492
|
/**
|
|
@@ -793,6 +819,10 @@ export async function retrieveKnowledge(
|
|
|
793
819
|
|
|
794
820
|
|
|
795
821
|
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
|
|
796
826
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:lexicalSearchEmbeddings:function" type="function" identity="lexicalSearchEmbeddings" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
797
827
|
/**
|
|
798
828
|
* Performs a lexical search on embeddings based on a query and specified filters. This function retrieves relevant documents from the database while considering account permissions and project associations.
|
|
@@ -871,6 +901,10 @@ async function lexicalSearchEmbeddings(
|
|
|
871
901
|
|
|
872
902
|
|
|
873
903
|
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
|
|
874
908
|
|
|
875
909
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:getConversationHistory:function" type="function" identity="getConversationHistory" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
876
910
|
/**
|
|
@@ -929,6 +963,10 @@ async function getConversationHistory(
|
|
|
929
963
|
|
|
930
964
|
|
|
931
965
|
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
|
|
932
970
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:recoverUserQuestion:function" type="function" identity="recoverUserQuestion" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
933
971
|
/**
|
|
934
972
|
* Retrieves the most recent inbound user question from the database based on the provided thread ID. It updates the state with the question if it is not already set.
|
|
@@ -970,6 +1008,10 @@ async function recoverUserQuestion(threadId: string, state: ResearchState): Prom
|
|
|
970
1008
|
|
|
971
1009
|
|
|
972
1010
|
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
|
|
973
1015
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:callInference:function" type="function" identity="callInference" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
974
1016
|
/**
|
|
975
1017
|
* The callInference function performs inference using a specified agent and context, returning the result of the inference process. It handles both live inference through an external API and a mock response when the API is unavailable.
|
|
@@ -1077,6 +1119,10 @@ async function callInference(
|
|
|
1077
1119
|
|
|
1078
1120
|
|
|
1079
1121
|
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
|
|
1080
1126
|
|
|
1081
1127
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeInferenceHandler:function" type="function" identity="executeInferenceHandler" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1082
1128
|
/**
|
|
@@ -1148,6 +1194,10 @@ async function executeInferenceHandler(
|
|
|
1148
1194
|
|
|
1149
1195
|
|
|
1150
1196
|
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
|
|
1151
1201
|
|
|
1152
1202
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:dispatchTools:function" type="function" identity="dispatchTools" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1153
1203
|
/**
|
|
@@ -1262,6 +1312,10 @@ async function dispatchTools(
|
|
|
1262
1312
|
|
|
1263
1313
|
|
|
1264
1314
|
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
|
|
1265
1319
|
|
|
1266
1320
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:cilDenied:function" type="function" identity="cilDenied" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1267
1321
|
/**
|
|
@@ -1306,6 +1360,10 @@ function cilDenied(
|
|
|
1306
1360
|
|
|
1307
1361
|
|
|
1308
1362
|
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
|
|
1309
1367
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:cilAllowedProjectIds:function" type="function" identity="cilAllowedProjectIds" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1310
1368
|
/**
|
|
1311
1369
|
* Retrieves a list of project IDs that are allowed for the given run context, returning null if access is denied. This function is essential for determining which projects can be interacted with based on the provided configuration.
|
|
@@ -1344,6 +1402,10 @@ function cilAllowedProjectIds(
|
|
|
1344
1402
|
|
|
1345
1403
|
|
|
1346
1404
|
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
|
|
1347
1409
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:CilAccess:interface" type="interface" identity="CilAccess" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1348
1410
|
/**
|
|
1349
1411
|
* The CilAccess interface defines the structure for managing access control by specifying associated account and project identifiers. It exists to facilitate the organization and retrieval of access-related information within the module.
|
|
@@ -1373,6 +1435,10 @@ export interface CilAccess {
|
|
|
1373
1435
|
|
|
1374
1436
|
|
|
1375
1437
|
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
|
|
1376
1442
|
|
|
1377
1443
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:resolveCilAccess:function" type="function" identity="resolveCilAccess" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1378
1444
|
/**
|
|
@@ -1423,6 +1489,10 @@ async function resolveCilAccess(
|
|
|
1423
1489
|
|
|
1424
1490
|
|
|
1425
1491
|
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
|
|
1426
1496
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:RetrievalPolicy:type" type="type" identity="RetrievalPolicy" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1427
1497
|
/**
|
|
1428
1498
|
* RetrievalPolicy defines the criteria for retrieving items based on various attributes such as security level and project IDs. It provides a structured way to enforce retrieval constraints in the agent runner module.
|
|
@@ -1471,6 +1541,10 @@ export type RetrievalPolicy = {
|
|
|
1471
1541
|
|
|
1472
1542
|
|
|
1473
1543
|
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
|
|
1474
1548
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:RetrievalCacheEntry:type" type="type" identity="RetrievalCacheEntry" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1475
1549
|
/**
|
|
1476
1550
|
* RetrievalCacheEntry represents an entry in a retrieval cache, storing documents and their similarity metrics. It is used to manage and evaluate cached retrieval results efficiently.
|
|
@@ -1487,6 +1561,10 @@ type RetrievalCacheEntry = {
|
|
|
1487
1561
|
}
|
|
1488
1562
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:RetrievalCacheEntry:type"]
|
|
1489
1563
|
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
|
|
1490
1568
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:RetrievalCache:type" type="type" identity="RetrievalCache" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1491
1569
|
/**
|
|
1492
1570
|
* RetrievalCache is a type that maps string keys to RetrievalCacheEntry objects, facilitating efficient storage and retrieval of cached data entries. It exists to provide a structured way to manage cached retrieval results in the module.
|
|
@@ -1498,6 +1576,10 @@ type RetrievalCacheEntry = {
|
|
|
1498
1576
|
export type RetrievalCache = Record<string, RetrievalCacheEntry>
|
|
1499
1577
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:RetrievalCache:type"]
|
|
1500
1578
|
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
|
|
1501
1583
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:RETRIEVAL_CACHE_MAX_ENTRIES:constant" type="constant" identity="RETRIEVAL_CACHE_MAX_ENTRIES" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1502
1584
|
/**
|
|
1503
1585
|
* RETRIEVAL_CACHE_MAX_ENTRIES defines the maximum number of entries allowed in the retrieval cache. This constant is used to limit the size of the cache for performance optimization.
|
|
@@ -1509,6 +1591,10 @@ export type RetrievalCache = Record<string, RetrievalCacheEntry>
|
|
|
1509
1591
|
const RETRIEVAL_CACHE_MAX_ENTRIES = 4
|
|
1510
1592
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:RETRIEVAL_CACHE_MAX_ENTRIES:constant"]
|
|
1511
1593
|
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
|
|
1512
1598
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:normalizeRetrievalQuery:function" type="function" identity="normalizeRetrievalQuery" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1513
1599
|
/**
|
|
1514
1600
|
* Normalizes a retrieval query string by trimming whitespace, converting it to lowercase, and reducing multiple spaces to a single space. This ensures consistent formatting for query processing.
|
|
@@ -1525,6 +1611,10 @@ function normalizeRetrievalQuery(q: string): string {
|
|
|
1525
1611
|
}
|
|
1526
1612
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:normalizeRetrievalQuery:function"]
|
|
1527
1613
|
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
|
|
1528
1618
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:retrievalCacheKey:function" type="function" identity="retrievalCacheKey" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1529
1619
|
/**
|
|
1530
1620
|
* Generates a unique cache key for retrieval operations based on the provided query and policy parameters. This key is used to optimize retrieval performance by caching results.
|
|
@@ -1563,6 +1653,10 @@ function retrievalCacheKey(
|
|
|
1563
1653
|
}
|
|
1564
1654
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:retrievalCacheKey:function"]
|
|
1565
1655
|
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
|
|
1566
1660
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:retrievalCacheStore:function" type="function" identity="retrievalCacheStore" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1567
1661
|
/**
|
|
1568
1662
|
* Stores an entry in the retrieval cache, ensuring that the cache does not exceed a maximum number of entries by removing the oldest entries. This function helps manage memory usage and maintain the relevance of cached data.
|
|
@@ -1601,6 +1695,10 @@ function retrievalCacheStore(cache: RetrievalCache, key: string, entry: Retrieva
|
|
|
1601
1695
|
|
|
1602
1696
|
|
|
1603
1697
|
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
|
|
1604
1702
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:resolveRetrievalPolicy:function" type="function" identity="resolveRetrievalPolicy" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1605
1703
|
/**
|
|
1606
1704
|
* Resolves the retrieval policy based on the provided run context and its configuration, ensuring that only allowed project IDs are included in the final policy. This function is essential for maintaining access control in retrieval operations.
|
|
@@ -1660,6 +1758,10 @@ function resolveRetrievalPolicy(
|
|
|
1660
1758
|
|
|
1661
1759
|
|
|
1662
1760
|
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
|
|
1663
1765
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeToolAction:function" type="function" identity="executeToolAction" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1664
1766
|
/**
|
|
1665
1767
|
* Executes a specified tool action based on the provided parameters and context, serving as a dispatcher for various action handlers. This allows for flexible integration with different services and workflows.
|
|
@@ -1802,6 +1904,10 @@ export async function executeToolAction(
|
|
|
1802
1904
|
|
|
1803
1905
|
|
|
1804
1906
|
|
|
1907
|
+
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
|
|
1805
1911
|
|
|
1806
1912
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeEscalateToHuman:function" type="function" identity="executeEscalateToHuman" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1807
1913
|
/**
|
|
@@ -1849,11 +1955,11 @@ async function executeEscalateToHuman(
|
|
|
1849
1955
|
.select('data')
|
|
1850
1956
|
.eq('id', threadId)
|
|
1851
1957
|
.single()
|
|
1852
|
-
if (thread?.data
|
|
1958
|
+
if (isHumanOwned(thread?.data)) {
|
|
1853
1959
|
return { escalated: true, already_escalated: true }
|
|
1854
1960
|
}
|
|
1855
1961
|
const confidence = typeof params?.confidence === 'number' ? params.confidence : 0
|
|
1856
|
-
await handleEscalation(threadId, confidence, promptConfig, ctx, 'agent_requested')
|
|
1962
|
+
await handleEscalation(threadId, confidence, promptConfig, ctx, 'agent_requested', runCtx?.config?.leaseToken)
|
|
1857
1963
|
return { escalated: true, already_escalated: false, reason: params?.reason || null }
|
|
1858
1964
|
}
|
|
1859
1965
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:executeEscalateToHuman:function"]
|
|
@@ -1873,6 +1979,10 @@ async function executeEscalateToHuman(
|
|
|
1873
1979
|
|
|
1874
1980
|
|
|
1875
1981
|
|
|
1982
|
+
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
|
|
1876
1986
|
|
|
1877
1987
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeSearchKnowledge:function" type="function" identity="executeSearchKnowledge" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1878
1988
|
/**
|
|
@@ -1951,6 +2061,8 @@ async function executeSearchKnowledge(
|
|
|
1951
2061
|
|
|
1952
2062
|
|
|
1953
2063
|
|
|
2064
|
+
|
|
2065
|
+
|
|
1954
2066
|
// Entities a model-driven query may read. Sensitive tables (api_keys,
|
|
1955
2067
|
// integrations, triggers, pipelines, people, audit/logs) are excluded —
|
|
1956
2068
|
// this tool exists for runtime records, not platform internals.
|
|
@@ -1961,6 +2073,8 @@ async function executeSearchKnowledge(
|
|
|
1961
2073
|
|
|
1962
2074
|
|
|
1963
2075
|
|
|
2076
|
+
|
|
2077
|
+
|
|
1964
2078
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:QUERY_ITEMS_ENTITIES:constant" type="constant" identity="QUERY_ITEMS_ENTITIES" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
1965
2079
|
/**
|
|
1966
2080
|
* QUERY_ITEMS_ENTITIES is a Set that contains the names of various entity types used in the application. It provides a centralized collection of these entity names for easier management and reference throughout the module.
|
|
@@ -2005,6 +2119,10 @@ const QUERY_ITEMS_ENTITIES = new Set([
|
|
|
2005
2119
|
|
|
2006
2120
|
|
|
2007
2121
|
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
|
|
2008
2126
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeQueryItems:function" type="function" identity="executeQueryItems" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2009
2127
|
/**
|
|
2010
2128
|
* Executes a query to retrieve items from a specified entity in the database, applying optional filters and a limit on the number of results. This function ensures that the entity is valid and handles potential query errors.
|
|
@@ -2075,6 +2193,10 @@ async function executeQueryItems(
|
|
|
2075
2193
|
|
|
2076
2194
|
|
|
2077
2195
|
|
|
2196
|
+
|
|
2197
|
+
|
|
2198
|
+
|
|
2199
|
+
|
|
2078
2200
|
|
|
2079
2201
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeCreateRecord:function" type="function" identity="executeCreateRecord" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2080
2202
|
/**
|
|
@@ -2145,6 +2267,10 @@ async function executeCreateRecord(
|
|
|
2145
2267
|
|
|
2146
2268
|
|
|
2147
2269
|
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
|
|
2273
|
+
|
|
2148
2274
|
|
|
2149
2275
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeUpdateItem:function" type="function" identity="executeUpdateItem" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2150
2276
|
/**
|
|
@@ -2212,6 +2338,10 @@ async function executeUpdateItem(
|
|
|
2212
2338
|
|
|
2213
2339
|
|
|
2214
2340
|
|
|
2341
|
+
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
|
|
2215
2345
|
|
|
2216
2346
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeSendNotification:function" type="function" identity="executeSendNotification" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2217
2347
|
/**
|
|
@@ -2285,6 +2415,10 @@ async function executeSendNotification(
|
|
|
2285
2415
|
|
|
2286
2416
|
|
|
2287
2417
|
|
|
2418
|
+
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
|
|
2288
2422
|
|
|
2289
2423
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:handleEscalation:function" type="function" identity="handleEscalation" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2290
2424
|
/**
|
|
@@ -2326,23 +2460,32 @@ async function handleEscalation(
|
|
|
2326
2460
|
confidence: number,
|
|
2327
2461
|
promptConfig: any,
|
|
2328
2462
|
ctx: CoreContext,
|
|
2329
|
-
escalationReason: string = 'low_confidence'
|
|
2463
|
+
escalationReason: string = 'low_confidence',
|
|
2464
|
+
leaseToken?: string
|
|
2330
2465
|
): Promise<void> {
|
|
2331
2466
|
return withSpan(ctx, 'agent.escalation', async (ctx) => {
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
.
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2467
|
+
// Atomic key merge — a whole-blob read-modify-write here could clobber
|
|
2468
|
+
// a research_state checkpoint written meanwhile (and vice versa).
|
|
2469
|
+
// Lease-fenced when the caller holds the episode lease.
|
|
2470
|
+
const merged = await mergeThreadData(threadId, { owner: 'human' }, leaseToken)
|
|
2471
|
+
if (merged === false) throw new EpisodeLeaseLost(threadId)
|
|
2472
|
+
if (merged === null) {
|
|
2473
|
+
// RPC unavailable — legacy whole-blob write.
|
|
2474
|
+
const { data: currentThread } = await adminDb
|
|
2475
|
+
.from('threads')
|
|
2476
|
+
.select('data')
|
|
2477
|
+
.eq('id', threadId)
|
|
2478
|
+
.single()
|
|
2479
|
+
await adminDb
|
|
2480
|
+
.from('threads')
|
|
2481
|
+
.update({
|
|
2482
|
+
data: {
|
|
2483
|
+
...(currentThread?.data || {}),
|
|
2484
|
+
owner: 'human'
|
|
2485
|
+
}
|
|
2486
|
+
})
|
|
2487
|
+
.eq('id', threadId)
|
|
2488
|
+
}
|
|
2346
2489
|
const { data: thread } = await adminDb
|
|
2347
2490
|
.from('threads')
|
|
2348
2491
|
.select('target_type, target_id')
|
|
@@ -2438,6 +2581,10 @@ async function handleEscalation(
|
|
|
2438
2581
|
|
|
2439
2582
|
|
|
2440
2583
|
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
|
|
2587
|
+
|
|
2441
2588
|
|
|
2442
2589
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:getReplyTypeSlug:function" type="function" identity="getReplyTypeSlug" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2443
2590
|
/**
|
|
@@ -2488,6 +2635,8 @@ export function getReplyTypeSlug(threadType: any): string {
|
|
|
2488
2635
|
|
|
2489
2636
|
|
|
2490
2637
|
|
|
2638
|
+
|
|
2639
|
+
|
|
2491
2640
|
/**
|
|
2492
2641
|
* Saves a message to the database associated with a specific thread, determining its direction and visibility based on the message type. This function is essential for maintaining the state of conversations in the agent framework.
|
|
2493
2642
|
*
|
|
@@ -2530,6 +2679,8 @@ export function getReplyTypeSlug(threadType: any): string {
|
|
|
2530
2679
|
// stays the correctness backstop: an out-of-band writer (customer message
|
|
2531
2680
|
// mid-episode, second instance) makes a stale counter surface as 23505, which
|
|
2532
2681
|
// triggers a reseed + retry inside saveMessage.
|
|
2682
|
+
|
|
2683
|
+
|
|
2533
2684
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:threadSeqCounter:constant" type="constant" identity="threadSeqCounter" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2534
2685
|
/**
|
|
2535
2686
|
* The threadSeqCounter is a Map that associates string keys with numeric values, serving as a counter for thread sequences. It exists to efficiently track and manage the sequence numbers of threads in the agent runner module.
|
|
@@ -2540,6 +2691,10 @@ export function getReplyTypeSlug(threadType: any): string {
|
|
|
2540
2691
|
*/
|
|
2541
2692
|
const threadSeqCounter = new Map<string, number>()
|
|
2542
2693
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:threadSeqCounter:constant"]
|
|
2694
|
+
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
|
|
2543
2698
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:threadSeqSeeding:constant" type="constant" identity="threadSeqSeeding" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2544
2699
|
/**
|
|
2545
2700
|
* A map that associates string keys with promises that resolve to void. It is used to manage asynchronous operations related to thread sequencing in the agent runner module.
|
|
@@ -2551,6 +2706,10 @@ const threadSeqCounter = new Map<string, number>()
|
|
|
2551
2706
|
const threadSeqSeeding = new Map<string, Promise<void>>()
|
|
2552
2707
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:threadSeqSeeding:constant"]
|
|
2553
2708
|
|
|
2709
|
+
|
|
2710
|
+
|
|
2711
|
+
|
|
2712
|
+
|
|
2554
2713
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:ensureSequenceCounter:function" type="function" identity="ensureSequenceCounter" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2555
2714
|
/**
|
|
2556
2715
|
* Ensures that a sequence counter for a given thread ID is initialized and updated based on the count of messages in the database. This function prevents duplicate initializations by using a seeding mechanism.
|
|
@@ -2581,8 +2740,12 @@ async function ensureSequenceCounter(threadId: string): Promise<void> {
|
|
|
2581
2740
|
}
|
|
2582
2741
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:ensureSequenceCounter:function"]
|
|
2583
2742
|
|
|
2743
|
+
|
|
2744
|
+
|
|
2584
2745
|
// Sync allocation — callers that batch writes (tool_call markers) allocate all
|
|
2585
2746
|
// sequences up front so a Promise.all of inserts preserves display order.
|
|
2747
|
+
|
|
2748
|
+
|
|
2586
2749
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:allocSequence:function" type="function" identity="allocSequence" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2587
2750
|
/**
|
|
2588
2751
|
* Allocates a sequence number for a given thread ID, ensuring unique sequence numbers for each thread. This function is essential for managing thread-specific operations in a concurrent environment.
|
|
@@ -2601,6 +2764,10 @@ function allocSequence(threadId: string): number {
|
|
|
2601
2764
|
}
|
|
2602
2765
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:allocSequence:function"]
|
|
2603
2766
|
|
|
2767
|
+
|
|
2768
|
+
|
|
2769
|
+
|
|
2770
|
+
|
|
2604
2771
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:saveMessage:function" type="function" identity="saveMessage" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2605
2772
|
/**
|
|
2606
2773
|
* Saves a message to a specified thread with relevant metadata. It ensures message sequence integrity and handles potential duplicate key errors during insertion.
|
|
@@ -2690,6 +2857,10 @@ async function saveMessage(
|
|
|
2690
2857
|
|
|
2691
2858
|
|
|
2692
2859
|
|
|
2860
|
+
|
|
2861
|
+
|
|
2862
|
+
|
|
2863
|
+
|
|
2693
2864
|
|
|
2694
2865
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:AgentConfig:interface" type="interface" identity="AgentConfig" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
2695
2866
|
/**
|
|
@@ -2722,6 +2893,8 @@ export interface AgentConfig {
|
|
|
2722
2893
|
threadType: any
|
|
2723
2894
|
/** Episode lease owner — stamped into research_state on every checkpoint. */
|
|
2724
2895
|
driverToken?: string
|
|
2896
|
+
/** DB lease token from acquire_episode_lease — heartbeats per turn and fences takeover writes. */
|
|
2897
|
+
leaseToken?: string
|
|
2725
2898
|
}
|
|
2726
2899
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:AgentConfig:interface"]
|
|
2727
2900
|
|
|
@@ -2740,6 +2913,10 @@ export interface AgentConfig {
|
|
|
2740
2913
|
|
|
2741
2914
|
|
|
2742
2915
|
|
|
2916
|
+
|
|
2917
|
+
|
|
2918
|
+
|
|
2919
|
+
|
|
2743
2920
|
|
|
2744
2921
|
|
|
2745
2922
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:InferenceResult:interface" type="interface" identity="InferenceResult" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
@@ -2789,6 +2966,10 @@ export interface InferenceResult {
|
|
|
2789
2966
|
|
|
2790
2967
|
|
|
2791
2968
|
|
|
2969
|
+
|
|
2970
|
+
|
|
2971
|
+
|
|
2972
|
+
|
|
2792
2973
|
|
|
2793
2974
|
|
|
2794
2975
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:ToolCall:interface" type="interface" identity="ToolCall" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
@@ -2837,6 +3018,10 @@ export interface ToolCall {
|
|
|
2837
3018
|
|
|
2838
3019
|
|
|
2839
3020
|
|
|
3021
|
+
|
|
3022
|
+
|
|
3023
|
+
|
|
3024
|
+
|
|
2840
3025
|
|
|
2841
3026
|
|
|
2842
3027
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:ToolResult:interface" type="interface" identity="ToolResult" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
@@ -2893,6 +3078,10 @@ export interface ToolResult {
|
|
|
2893
3078
|
|
|
2894
3079
|
|
|
2895
3080
|
|
|
3081
|
+
|
|
3082
|
+
|
|
3083
|
+
|
|
3084
|
+
|
|
2896
3085
|
|
|
2897
3086
|
|
|
2898
3087
|
|
|
@@ -3055,6 +3244,10 @@ const CIL_TOOLS: any[] = [
|
|
|
3055
3244
|
|
|
3056
3245
|
|
|
3057
3246
|
|
|
3247
|
+
|
|
3248
|
+
|
|
3249
|
+
|
|
3250
|
+
|
|
3058
3251
|
|
|
3059
3252
|
|
|
3060
3253
|
|
|
@@ -3094,6 +3287,10 @@ const CIL_TOOL_SLUGS = new Set(CIL_TOOLS.map(t => t.name))
|
|
|
3094
3287
|
|
|
3095
3288
|
|
|
3096
3289
|
|
|
3290
|
+
|
|
3291
|
+
|
|
3292
|
+
|
|
3293
|
+
|
|
3097
3294
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:CIL_ONLY_TOOL_SLUGS:constant" type="constant" identity="CIL_ONLY_TOOL_SLUGS" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3098
3295
|
/**
|
|
3099
3296
|
* CIL_ONLY_TOOL_SLUGS is a set that contains tool slugs prefixed with 'cil_' from the CIL_TOOL_SLUGS collection. It exists to filter and provide a specific subset of tool slugs for CIL-related operations.
|
|
@@ -3121,6 +3318,8 @@ const CIL_ONLY_TOOL_SLUGS = new Set([...CIL_TOOL_SLUGS].filter((n: string) => n.
|
|
|
3121
3318
|
|
|
3122
3319
|
|
|
3123
3320
|
|
|
3321
|
+
|
|
3322
|
+
|
|
3124
3323
|
// Tools that count as "research" for the ask_user gate: every built-in CIL tool
|
|
3125
3324
|
// except ask_user itself and the always-available escalate_to_human handoff.
|
|
3126
3325
|
|
|
@@ -3130,6 +3329,8 @@ const CIL_ONLY_TOOL_SLUGS = new Set([...CIL_TOOL_SLUGS].filter((n: string) => n.
|
|
|
3130
3329
|
|
|
3131
3330
|
|
|
3132
3331
|
|
|
3332
|
+
|
|
3333
|
+
|
|
3133
3334
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:RESEARCH_TOOLS:constant" type="constant" identity="RESEARCH_TOOLS" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3134
3335
|
/**
|
|
3135
3336
|
* RESEARCH_TOOLS is a filtered collection of tool names derived from CIL_TOOLS, excluding 'ask_user' and 'escalate_to_human'. It provides a focused set of tools for research purposes within the agent runner module.
|
|
@@ -3175,6 +3376,10 @@ const RESEARCH_TOOLS = new Set(
|
|
|
3175
3376
|
|
|
3176
3377
|
|
|
3177
3378
|
|
|
3379
|
+
|
|
3380
|
+
|
|
3381
|
+
|
|
3382
|
+
|
|
3178
3383
|
|
|
3179
3384
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:computeCilChunkHash:function" type="function" identity="computeCilChunkHash" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3180
3385
|
/**
|
|
@@ -3224,6 +3429,10 @@ function computeCilChunkHash(content: string): string {
|
|
|
3224
3429
|
|
|
3225
3430
|
|
|
3226
3431
|
|
|
3432
|
+
|
|
3433
|
+
|
|
3434
|
+
|
|
3435
|
+
|
|
3227
3436
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:ResearchState:interface" type="interface" identity="ResearchState" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3228
3437
|
/**
|
|
3229
3438
|
* The ResearchState interface defines the structure for managing the state of a research process, including its status and relevant metadata. It facilitates tracking and updating the progress of research tasks within the application.
|
|
@@ -3269,6 +3478,13 @@ interface ResearchState {
|
|
|
3269
3478
|
// on every checkpoint so other drivers refuse to interleave writes.
|
|
3270
3479
|
driver_id?: string
|
|
3271
3480
|
driver_heartbeat_at?: string
|
|
3481
|
+
// Set by the claim path when it writes the intake narration — runAgent
|
|
3482
|
+
// skips its own narration when the flag was stamped by the same driver.
|
|
3483
|
+
intake_announced?: boolean
|
|
3484
|
+
// Stamped when the episode reaches 'complete'/'failed' — lets a later
|
|
3485
|
+
// claim of the same question tell a replay (inbound predates the
|
|
3486
|
+
// terminal write) from a genuine same-text re-ask (inbound is newer).
|
|
3487
|
+
completed_at?: string
|
|
3272
3488
|
}
|
|
3273
3489
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:ResearchState:interface"]
|
|
3274
3490
|
|
|
@@ -3287,6 +3503,10 @@ interface ResearchState {
|
|
|
3287
3503
|
|
|
3288
3504
|
|
|
3289
3505
|
|
|
3506
|
+
|
|
3507
|
+
|
|
3508
|
+
|
|
3509
|
+
|
|
3290
3510
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:getResearchState:function" type="function" identity="getResearchState" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3291
3511
|
/**
|
|
3292
3512
|
* Retrieves the current research state from a thread, returning a default state if none exists. This function ensures that a valid research state is always available for processing.
|
|
@@ -3348,6 +3568,10 @@ function getResearchState(thread: any): ResearchState {
|
|
|
3348
3568
|
|
|
3349
3569
|
|
|
3350
3570
|
|
|
3571
|
+
|
|
3572
|
+
|
|
3573
|
+
|
|
3574
|
+
|
|
3351
3575
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:computeCompositeConfidence:function" type="function" identity="computeCompositeConfidence" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3352
3576
|
/**
|
|
3353
3577
|
* Computes a composite confidence score based on a language model confidence and mean similarity. This function adjusts the score based on specific thresholds of mean similarity to ensure the output remains within a defined range.
|
|
@@ -3404,6 +3628,10 @@ function computeCompositeConfidence(llmConf: number, meanSimilarity: number): nu
|
|
|
3404
3628
|
|
|
3405
3629
|
|
|
3406
3630
|
|
|
3631
|
+
|
|
3632
|
+
|
|
3633
|
+
|
|
3634
|
+
|
|
3407
3635
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:EvidenceState:type" type="type" identity="EvidenceState" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3408
3636
|
/**
|
|
3409
3637
|
* EvidenceState defines the possible states of evidence in the context of verification processes. It provides a clear and consistent representation of evidence status throughout the module.
|
|
@@ -3446,6 +3674,8 @@ export type EvidenceState = 'verified' | 'evidence_found' | 'incomplete' | 'not_
|
|
|
3446
3674
|
|
|
3447
3675
|
|
|
3448
3676
|
|
|
3677
|
+
|
|
3678
|
+
|
|
3449
3679
|
// Retrieval-time evidence state for a response. 'verified' is deliberately
|
|
3450
3680
|
// unreachable here — it is reserved for downstream human approval or eval
|
|
3451
3681
|
// confirmation; inference can never self-certify it.
|
|
@@ -3485,6 +3715,8 @@ export type EvidenceState = 'verified' | 'evidence_found' | 'incomplete' | 'not_
|
|
|
3485
3715
|
|
|
3486
3716
|
// A single rank-1 similarity at or above this is decisive evidence on its own —
|
|
3487
3717
|
// below it, a lone top hit could still be a low-signal lexical floater.
|
|
3718
|
+
|
|
3719
|
+
|
|
3488
3720
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:TOP_HIT_SIMILARITY:constant" type="constant" identity="TOP_HIT_SIMILARITY" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3489
3721
|
/**
|
|
3490
3722
|
* TOP_HIT_SIMILARITY defines a constant value used to determine the threshold for similarity in hit scoring. It exists to provide a standard reference point for comparison across the module.
|
|
@@ -3496,6 +3728,10 @@ export type EvidenceState = 'verified' | 'evidence_found' | 'incomplete' | 'not_
|
|
|
3496
3728
|
const TOP_HIT_SIMILARITY = 0.5
|
|
3497
3729
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:TOP_HIT_SIMILARITY:constant"]
|
|
3498
3730
|
|
|
3731
|
+
|
|
3732
|
+
|
|
3733
|
+
|
|
3734
|
+
|
|
3499
3735
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:computeEvidenceState:function" type="function" identity="computeEvidenceState" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3500
3736
|
/**
|
|
3501
3737
|
* Computes the evidence state based on the provided documents and their mean similarity score. It determines whether evidence is found, incomplete, or not established based on specific thresholds.
|
|
@@ -3525,6 +3761,10 @@ export function computeEvidenceState(
|
|
|
3525
3761
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:computeEvidenceState:function"]
|
|
3526
3762
|
|
|
3527
3763
|
|
|
3764
|
+
|
|
3765
|
+
|
|
3766
|
+
|
|
3767
|
+
|
|
3528
3768
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:hasInRangeCitationMarker:function" type="function" identity="hasInRangeCitationMarker" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3529
3769
|
/**
|
|
3530
3770
|
* Determines if the provided content contains at least one citation marker that corresponds to a valid source in the retrieved sources array. This function is essential for ensuring that evidence is properly cited in the content.
|
|
@@ -3568,6 +3808,10 @@ export function hasInRangeCitationMarker(content: string, sourceCount: number):
|
|
|
3568
3808
|
|
|
3569
3809
|
|
|
3570
3810
|
|
|
3811
|
+
|
|
3812
|
+
|
|
3813
|
+
|
|
3814
|
+
|
|
3571
3815
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:extractCitedSources:function" type="function" identity="extractCitedSources" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3572
3816
|
/**
|
|
3573
3817
|
* Extracts cited sources from the provided content based on reference numbers, returning either the cited sources or all sources if none are found. This function helps in identifying and organizing referenced materials within a text.
|
|
@@ -3622,6 +3866,10 @@ export function extractCitedSources(content: string, sources: any[]): any[] {
|
|
|
3622
3866
|
|
|
3623
3867
|
|
|
3624
3868
|
|
|
3869
|
+
|
|
3870
|
+
|
|
3871
|
+
|
|
3872
|
+
|
|
3625
3873
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:ensureInlineCitations:function" type="function" identity="ensureInlineCitations" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3626
3874
|
/**
|
|
3627
3875
|
* Ensures that inline citations are present in the provided content based on the given sources. If no citations are found, it appends a list of sources to the content.
|
|
@@ -3665,6 +3913,10 @@ export function ensureInlineCitations(content: string, sources: any[]): string {
|
|
|
3665
3913
|
|
|
3666
3914
|
|
|
3667
3915
|
|
|
3916
|
+
|
|
3917
|
+
|
|
3918
|
+
|
|
3919
|
+
|
|
3668
3920
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:shapeRetrievedSource:function" type="function" identity="shapeRetrievedSource" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3669
3921
|
/**
|
|
3670
3922
|
* The function shapeRetrievedSource transforms a source object into a structured format containing specific fields. This standardization facilitates further processing of retrieved sources.
|
|
@@ -3720,9 +3972,13 @@ function shapeRetrievedSource(s: any): any {
|
|
|
3720
3972
|
|
|
3721
3973
|
|
|
3722
3974
|
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3975
|
+
|
|
3976
|
+
|
|
3977
|
+
|
|
3978
|
+
|
|
3979
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:truncateForMessage:function" type="function" identity="truncateForMessage" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3980
|
+
/**
|
|
3981
|
+
* Truncate a value to a specified maximum character length for message display, ensuring that long values are shortened with a notice of truncation. This function is useful for maintaining readability in logs or messages where space is limited.
|
|
3726
3982
|
*
|
|
3727
3983
|
* @inputSpec
|
|
3728
3984
|
* value: any (required) — the value to be truncated.
|
|
@@ -3771,6 +4027,10 @@ function truncateForMessage(value: any, maxChars: number): any {
|
|
|
3771
4027
|
|
|
3772
4028
|
|
|
3773
4029
|
|
|
4030
|
+
|
|
4031
|
+
|
|
4032
|
+
|
|
4033
|
+
|
|
3774
4034
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:setResearchState:function" type="function" identity="setResearchState" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3775
4035
|
/**
|
|
3776
4036
|
* Updates the research state of a specified thread in the database, ensuring that the latest state is recorded along with the current data and a timestamp.
|
|
@@ -3804,13 +4064,27 @@ function truncateForMessage(value: any, maxChars: number): any {
|
|
|
3804
4064
|
async function setResearchState(
|
|
3805
4065
|
threadId: string,
|
|
3806
4066
|
state: ResearchState,
|
|
3807
|
-
currentData: any
|
|
4067
|
+
currentData: any,
|
|
4068
|
+
leaseToken?: string
|
|
3808
4069
|
): Promise<void> {
|
|
3809
4070
|
// Mirror the write into the caller's in-memory thread data — multi-step
|
|
3810
4071
|
// modes read getResearchState(config.thread) again on the next step and
|
|
3811
4072
|
// must see the state just persisted, not a stale snapshot.
|
|
3812
4073
|
if (currentData) currentData.research_state = state
|
|
3813
|
-
|
|
4074
|
+
// Atomic key-set via merge_thread_data: the previous read-modify-write
|
|
4075
|
+
// of the whole `data` blob could resurrect a stale research_state over
|
|
4076
|
+
// a terminal checkpoint when a concurrent writer (escalation stamp,
|
|
4077
|
+
// message-trigger pipeline) landed in between. The lease token fences
|
|
4078
|
+
// the write — a displaced driver's stale checkpoint is dropped.
|
|
4079
|
+
const merged = await mergeThreadData(threadId, { research_state: state }, leaseToken)
|
|
4080
|
+
if (merged === true) return
|
|
4081
|
+
if (merged === false) {
|
|
4082
|
+
// A takeover displaced this driver mid-run — its checkpoint must not
|
|
4083
|
+
// overwrite the new owner's state.
|
|
4084
|
+
throw new EpisodeLeaseLost(threadId)
|
|
4085
|
+
}
|
|
4086
|
+
// RPC unavailable (un-migrated deployment) — legacy whole-blob write.
|
|
4087
|
+
const { error } = await adminDb
|
|
3814
4088
|
.from('threads')
|
|
3815
4089
|
.update({
|
|
3816
4090
|
data: {
|
|
@@ -3820,6 +4094,7 @@ async function setResearchState(
|
|
|
3820
4094
|
updated_at: new Date().toISOString()
|
|
3821
4095
|
})
|
|
3822
4096
|
.eq('id', threadId)
|
|
4097
|
+
if (error) console.error(`setResearchState failed for thread ${threadId}:`, error.message)
|
|
3823
4098
|
}
|
|
3824
4099
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:setResearchState:function"]
|
|
3825
4100
|
|
|
@@ -3838,6 +4113,10 @@ async function setResearchState(
|
|
|
3838
4113
|
|
|
3839
4114
|
|
|
3840
4115
|
|
|
4116
|
+
|
|
4117
|
+
|
|
4118
|
+
|
|
4119
|
+
|
|
3841
4120
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:isEmptyToolResult:function" type="function" identity="isEmptyToolResult" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3842
4121
|
/**
|
|
3843
4122
|
* Determines whether a given tool result is empty or not by evaluating various data types and structures. This function is essential for validating the presence of meaningful data in tool results.
|
|
@@ -3895,6 +4174,10 @@ function isEmptyToolResult(result: any): boolean {
|
|
|
3895
4174
|
|
|
3896
4175
|
|
|
3897
4176
|
|
|
4177
|
+
|
|
4178
|
+
|
|
4179
|
+
|
|
4180
|
+
|
|
3898
4181
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:runAgentStep:function" type="function" identity="runAgentStep" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
3899
4182
|
/**
|
|
3900
4183
|
* Executes a single step in the agent's processing sequence, managing user input and context while coordinating research and inference tasks. This function is essential for maintaining the state and flow of interactions within the agent's operational framework.
|
|
@@ -3960,11 +4243,17 @@ export async function runAgentStep(
|
|
|
3960
4243
|
state.max_turns = maxTurns
|
|
3961
4244
|
// Refresh the single-flight lease on every step checkpoint — a live
|
|
3962
4245
|
// heartbeat tells other drivers (second mounts, step-mode fallbacks) to
|
|
3963
|
-
// watch instead of interleaving writes.
|
|
4246
|
+
// watch instead of interleaving writes. The DB lease is the atomic
|
|
4247
|
+
// authority: a false heartbeat means an expired-lease takeover
|
|
4248
|
+
// displaced this driver, so its remaining writes are fenced off.
|
|
3964
4249
|
if (config.driverToken) {
|
|
3965
4250
|
state.driver_id = config.driverToken
|
|
3966
4251
|
state.driver_heartbeat_at = new Date().toISOString()
|
|
3967
4252
|
}
|
|
4253
|
+
if (config.leaseToken) {
|
|
4254
|
+
const owned = await heartbeatEpisodeLease(threadId, config.leaseToken)
|
|
4255
|
+
if (owned === false) throw new EpisodeLeaseLost(threadId)
|
|
4256
|
+
}
|
|
3968
4257
|
|
|
3969
4258
|
// Retrieval is keyed on the user question, which is fixed for the whole
|
|
3970
4259
|
// episode — reuse it across turns and across timeout-resumed steps
|
|
@@ -4003,6 +4292,13 @@ export async function runAgentStep(
|
|
|
4003
4292
|
const inferenceAgent = researchAttempted || !agent.model_config?.tools?.length
|
|
4004
4293
|
? agent
|
|
4005
4294
|
: { ...agent, model_config: { ...agent.model_config, tools: agent.model_config.tools.filter((t: any) => t.function?.name !== 'ask_user') } }
|
|
4295
|
+
// Heartbeat again after buildContext — it can take tens of seconds,
|
|
4296
|
+
// and stale-gated detached steals key on heartbeat age. One extra RPC
|
|
4297
|
+
// keeps a healthy long-build driver unstealable through inference.
|
|
4298
|
+
if (config.leaseToken) {
|
|
4299
|
+
const owned = await heartbeatEpisodeLease(threadId, config.leaseToken)
|
|
4300
|
+
if (owned === false) throw new EpisodeLeaseLost(threadId)
|
|
4301
|
+
}
|
|
4006
4302
|
const inferenceResult = await callInference(context, inferenceAgent, promptConfig, ctx)
|
|
4007
4303
|
const confidence = computeCompositeConfidence(inferenceResult.confidence ?? 0.70, meanSimilarity)
|
|
4008
4304
|
const content = inferenceResult.content || ''
|
|
@@ -4075,6 +4371,23 @@ export async function runAgentStep(
|
|
|
4075
4371
|
// the ticket is already in the human queue via handleEscalation.
|
|
4076
4372
|
const escalated = toolResults.find((tr: any) => tr.tool === 'escalate_to_human' && tr.result?.escalated)
|
|
4077
4373
|
if (escalated) {
|
|
4374
|
+
state.status = 'complete'
|
|
4375
|
+
state.completed_at = new Date().toISOString()
|
|
4376
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4377
|
+
if (escalated.result?.already_escalated) {
|
|
4378
|
+
// Re-escalation on an already human-owned thread — the handoff
|
|
4379
|
+
// already happened and was already narrated; emit no second
|
|
4380
|
+
// customer-facing final. Return the prior outbound message so
|
|
4381
|
+
// callers still get terminal content, never a duplicate.
|
|
4382
|
+
const { data: priorOutbound } = await adminDb
|
|
4383
|
+
.from('messages')
|
|
4384
|
+
.select('id, content, data')
|
|
4385
|
+
.eq('thread_id', threadId)
|
|
4386
|
+
.eq('direction', 'outbound')
|
|
4387
|
+
.order('created_at', { ascending: false })
|
|
4388
|
+
.limit(1)
|
|
4389
|
+
return { message: (Array.isArray(priorOutbound) ? priorOutbound[0] : priorOutbound) || null, state, confidence, escalated: true }
|
|
4390
|
+
}
|
|
4078
4391
|
const escMsg = await saveMessage(threadId, "I've escalated this to a human support agent — they'll review your request and follow up shortly.", 'final', {
|
|
4079
4392
|
escalated: true,
|
|
4080
4393
|
escalation_notice: true,
|
|
@@ -4083,8 +4396,6 @@ export async function runAgentStep(
|
|
|
4083
4396
|
prompt_config_id: promptConfig.id,
|
|
4084
4397
|
sender_type: 'agent'
|
|
4085
4398
|
}, ctx, replyTypeSlug, 'external')
|
|
4086
|
-
state.status = 'complete'
|
|
4087
|
-
await setResearchState(threadId, state, thread.data)
|
|
4088
4399
|
return { message: escMsg, state, confidence, escalated: true }
|
|
4089
4400
|
}
|
|
4090
4401
|
|
|
@@ -4098,12 +4409,12 @@ export async function runAgentStep(
|
|
|
4098
4409
|
}, ctx, replyTypeSlug, 'external')
|
|
4099
4410
|
state.status = 'awaiting_input'
|
|
4100
4411
|
state.pending_question = question
|
|
4101
|
-
await setResearchState(threadId, state, thread.data)
|
|
4412
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4102
4413
|
return { message: askMsg, state }
|
|
4103
4414
|
}
|
|
4104
4415
|
|
|
4105
4416
|
state.status = 'gathering'
|
|
4106
|
-
await setResearchState(threadId, state, thread.data)
|
|
4417
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4107
4418
|
return { message: lastToolMessage, state }
|
|
4108
4419
|
}
|
|
4109
4420
|
|
|
@@ -4130,7 +4441,7 @@ export async function runAgentStep(
|
|
|
4130
4441
|
result: 'You attempted to ask the customer a question before any research tool ran this episode (research_attempted=false). Research first with search_knowledge and/or cil_chunk_lookup — an empty result still counts as an attempt — then answer, or ask once research has been attempted.',
|
|
4131
4442
|
})
|
|
4132
4443
|
state.status = 'gathering'
|
|
4133
|
-
await setResearchState(threadId, state, thread.data)
|
|
4444
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4134
4445
|
return { message: nudgeMsg, state }
|
|
4135
4446
|
}
|
|
4136
4447
|
const questionMsg = await saveMessage(threadId, content, 'awaiting_input', {
|
|
@@ -4141,7 +4452,7 @@ export async function runAgentStep(
|
|
|
4141
4452
|
}, ctx, replyTypeSlug, 'external')
|
|
4142
4453
|
state.status = 'awaiting_input'
|
|
4143
4454
|
state.pending_question = content
|
|
4144
|
-
await setResearchState(threadId, state, thread.data)
|
|
4455
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4145
4456
|
return { message: questionMsg, state }
|
|
4146
4457
|
}
|
|
4147
4458
|
|
|
@@ -4176,7 +4487,8 @@ export async function runAgentStep(
|
|
|
4176
4487
|
sender_type: 'agent'
|
|
4177
4488
|
}, ctx, replyTypeSlug, threadVisibility)
|
|
4178
4489
|
state.status = 'complete'
|
|
4179
|
-
|
|
4490
|
+
state.completed_at = new Date().toISOString()
|
|
4491
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4180
4492
|
// The message row comes back through createRecord → sanitizeRecordData,
|
|
4181
4493
|
// which drops data.* keys the message type doesn't declare — so callers
|
|
4182
4494
|
// must not read confidence/evidence_state off the returned record.
|
|
@@ -4200,6 +4512,10 @@ export async function runAgentStep(
|
|
|
4200
4512
|
|
|
4201
4513
|
|
|
4202
4514
|
|
|
4515
|
+
|
|
4516
|
+
|
|
4517
|
+
|
|
4518
|
+
|
|
4203
4519
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:runAgent:function" type="function" identity="runAgent" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
4204
4520
|
/**
|
|
4205
4521
|
* Runs an agent to process a user's message and generate a response based on the agent's configuration and context. This function is essential for handling interactions with the agent in a conversational setting.
|
|
@@ -4239,18 +4555,36 @@ export async function runAgent(
|
|
|
4239
4555
|
userMessage: string,
|
|
4240
4556
|
ctx: CoreContext,
|
|
4241
4557
|
mode: 'step' | 'complete' = 'complete',
|
|
4242
|
-
driverToken?: string
|
|
4558
|
+
driverToken?: string,
|
|
4559
|
+
leaseToken?: string,
|
|
4560
|
+
opts?: { deadlineMs?: number }
|
|
4243
4561
|
): Promise<any> {
|
|
4244
4562
|
return withSpan(ctx, 'agent.runAgent', async (ctx) => {
|
|
4245
4563
|
const startTime = Date.now()
|
|
4564
|
+
const deadlineMs = opts?.deadlineMs
|
|
4246
4565
|
let lastMessage: any
|
|
4247
4566
|
let lastStep: Awaited<ReturnType<typeof runAgentStep>> | undefined
|
|
4567
|
+
// Terminal release: the holder frees the lease only when the episode
|
|
4568
|
+
// reaches a terminal state or aborts — a nonterminal step-mode return
|
|
4569
|
+
// keeps the lease so the same driver's next HTTP request renews it.
|
|
4570
|
+
const releaseIfOwned = async (terminal: boolean) => {
|
|
4571
|
+
if (leaseToken && terminal) await releaseEpisodeLease(threadId, leaseToken)
|
|
4572
|
+
}
|
|
4248
4573
|
try {
|
|
4574
|
+
// Renew the lease before any prelude work — a detached driver that
|
|
4575
|
+
// was CPU-throttled between claim and run start would otherwise
|
|
4576
|
+
// risk its TTL expiring mid-prelude. A false heartbeat means a
|
|
4577
|
+
// steal/takeover already displaced us: abort before writing.
|
|
4578
|
+
if (leaseToken) {
|
|
4579
|
+
const owned = await heartbeatEpisodeLease(threadId, leaseToken)
|
|
4580
|
+
if (owned === false) throw new EpisodeLeaseLost(threadId)
|
|
4581
|
+
}
|
|
4249
4582
|
const config = await resolveAgentConfig(threadId, ctx)
|
|
4250
4583
|
if (!config) {
|
|
4251
4584
|
throw new Error(`Could not resolve agent configuration for thread ${threadId}`)
|
|
4252
4585
|
}
|
|
4253
4586
|
if (driverToken) config.driverToken = driverToken
|
|
4587
|
+
if (leaseToken) config.leaseToken = leaseToken
|
|
4254
4588
|
const { agent, promptConfig, thread, threadType } = config
|
|
4255
4589
|
const replyTypeSlug = getReplyTypeSlug(threadType)
|
|
4256
4590
|
const threadVisibility = thread?.visibility || 'external'
|
|
@@ -4278,6 +4612,22 @@ export async function runAgent(
|
|
|
4278
4612
|
if (!userMessage?.trim() && !state.user_question) {
|
|
4279
4613
|
await recoverUserQuestion(threadId, state)
|
|
4280
4614
|
}
|
|
4615
|
+
// Human-owner gate — re-validated at run start, after config resolve
|
|
4616
|
+
// gives a fresh thread read. A driver that claimed before the handoff
|
|
4617
|
+
// but resumes after it (a frozen serverless invocation waking into a
|
|
4618
|
+
// changed world) must not narrate or run turns. The inbound row above
|
|
4619
|
+
// already persisted — the human sees the message; the agent stays out.
|
|
4620
|
+
if (isHumanOwned(thread?.data)) {
|
|
4621
|
+
const { data: priorOutbound } = await adminDb
|
|
4622
|
+
.from('messages')
|
|
4623
|
+
.select('id, content, data')
|
|
4624
|
+
.eq('thread_id', threadId)
|
|
4625
|
+
.eq('direction', 'outbound')
|
|
4626
|
+
.order('created_at', { ascending: false })
|
|
4627
|
+
.limit(1)
|
|
4628
|
+
await releaseIfOwned(true)
|
|
4629
|
+
return (Array.isArray(priorOutbound) ? priorOutbound[0] : priorOutbound) || lastMessage
|
|
4630
|
+
}
|
|
4281
4631
|
const isMultiTurn = promptConfig.is_multi_turn !== false
|
|
4282
4632
|
if (!isMultiTurn) {
|
|
4283
4633
|
const { context, retrievedSources, meanSimilarity } = await buildContext(config, userMessage?.trim() || state.user_question || '', [], ctx)
|
|
@@ -4292,7 +4642,7 @@ export async function runAgent(
|
|
|
4292
4642
|
confidence < (promptConfig.confidence_threshold || 0) &&
|
|
4293
4643
|
evidenceState !== 'evidence_found'
|
|
4294
4644
|
if (isEscalated) {
|
|
4295
|
-
await handleEscalation(threadId, confidence, promptConfig, ctx)
|
|
4645
|
+
await handleEscalation(threadId, confidence, promptConfig, ctx, 'low_confidence', config.leaseToken)
|
|
4296
4646
|
}
|
|
4297
4647
|
// Same deterministic citation repair as the episode-mode path — an
|
|
4298
4648
|
// evidence_found answer must always carry an in-range [n] marker.
|
|
@@ -4340,22 +4690,34 @@ export async function runAgent(
|
|
|
4340
4690
|
confidence,
|
|
4341
4691
|
escalated: isEscalated
|
|
4342
4692
|
})
|
|
4693
|
+
await releaseIfOwned(true)
|
|
4343
4694
|
return lastMessage
|
|
4344
4695
|
}
|
|
4345
4696
|
|
|
4697
|
+
// A re-delivered copy of the question this episode already answered
|
|
4698
|
+
// (e.g. a late triage kick claiming the just-released lease) is not
|
|
4699
|
+
// a new episode — skipping the reset lets the terminal no-op below
|
|
4700
|
+
// return without re-running side-effect tools or duplicating finals.
|
|
4701
|
+
const questionReplay = await isAnsweredQuestionReplay(threadId, state, userMessage)
|
|
4702
|
+
|
|
4346
4703
|
// When a new user message arrives after the previous research episode
|
|
4347
4704
|
// reached a terminal state, start a fresh episode so the new question gets
|
|
4348
4705
|
// its own turn budget and gathered_results. 'awaiting_input' is excluded so
|
|
4349
4706
|
// the user's reply continues the pending clarification instead.
|
|
4350
|
-
if (userMessage?.trim() && (state.status === 'complete' || state.status === 'failed')) {
|
|
4707
|
+
if (userMessage?.trim() && !questionReplay && (state.status === 'complete' || state.status === 'failed')) {
|
|
4351
4708
|
state = {
|
|
4352
4709
|
status: 'planning',
|
|
4353
4710
|
turn_count: 0,
|
|
4354
4711
|
max_turns: state.max_turns,
|
|
4355
4712
|
gathered_results: [],
|
|
4713
|
+
// Carry intake/driver markers through the reset — the claim
|
|
4714
|
+
// path may already have narrated this new question's intake.
|
|
4715
|
+
intake_announced: state.intake_announced,
|
|
4716
|
+
driver_id: state.driver_id,
|
|
4717
|
+
driver_heartbeat_at: state.driver_heartbeat_at,
|
|
4356
4718
|
}
|
|
4357
4719
|
thread.data = { ...(thread.data || {}), research_state: state }
|
|
4358
|
-
await setResearchState(threadId, state, thread.data)
|
|
4720
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4359
4721
|
}
|
|
4360
4722
|
|
|
4361
4723
|
const maxTurns = promptConfig.metadata?.max_turns ?? (promptConfig.metadata?.max_tool_iterations ?? 5) + 2
|
|
@@ -4364,7 +4726,12 @@ export async function runAgent(
|
|
|
4364
4726
|
// start and the first inference result (~5–10s of buildContext + LLM
|
|
4365
4727
|
// call). Internal-only: activity trails render it, transcript filters
|
|
4366
4728
|
// skip it. Fire-and-forget so it never adds latency to the step.
|
|
4367
|
-
|
|
4729
|
+
// Skipped when the claim path already narrated this intake for the
|
|
4730
|
+
// current driver (claimAgentEpisode writes it synchronously so the
|
|
4731
|
+
// UI has progress even while a detached driver is CPU-throttled).
|
|
4732
|
+
const alreadyAnnounced = state.intake_announced === true
|
|
4733
|
+
&& !!driverToken && state.driver_id === driverToken
|
|
4734
|
+
if (state.turn_count === 0 && state.status === 'planning' && !alreadyAnnounced) {
|
|
4368
4735
|
void saveMessage(threadId, 'Reviewing your question…', 'plan', {
|
|
4369
4736
|
turn_count: 0,
|
|
4370
4737
|
phase: 'intake',
|
|
@@ -4386,8 +4753,9 @@ export async function runAgent(
|
|
|
4386
4753
|
// duplicate final message. Calls carrying a userMessage are unaffected:
|
|
4387
4754
|
// 'complete'/'failed' were reset to a fresh episode above and
|
|
4388
4755
|
// 'awaiting_input' replies continue the pending clarification.
|
|
4389
|
-
if (!currentMessage?.trim() &&
|
|
4756
|
+
if ((!currentMessage?.trim() || questionReplay) &&
|
|
4390
4757
|
(state.status === 'complete' || state.status === 'awaiting_input' || state.status === 'failed')) {
|
|
4758
|
+
await releaseIfOwned(true)
|
|
4391
4759
|
return lastMessage
|
|
4392
4760
|
}
|
|
4393
4761
|
if (mode === 'step') {
|
|
@@ -4403,7 +4771,9 @@ export async function runAgent(
|
|
|
4403
4771
|
}, ctx, replyTypeSlug, 'external')
|
|
4404
4772
|
state.status = 'failed'
|
|
4405
4773
|
state.error_message = `Research did not complete after ${state.turn_count} turns`
|
|
4406
|
-
|
|
4774
|
+
state.completed_at = new Date().toISOString()
|
|
4775
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4776
|
+
await releaseIfOwned(true)
|
|
4407
4777
|
return failMsg
|
|
4408
4778
|
}
|
|
4409
4779
|
const step = await runAgentStep(threadId, currentMessage, ctx, config)
|
|
@@ -4423,7 +4793,7 @@ export async function runAgent(
|
|
|
4423
4793
|
if (!finalEscalated &&
|
|
4424
4794
|
finalConfidence < (promptConfig.confidence_threshold || 0) &&
|
|
4425
4795
|
finalEvidence !== 'evidence_found') {
|
|
4426
|
-
await handleEscalation(threadId, finalConfidence, promptConfig, ctx)
|
|
4796
|
+
await handleEscalation(threadId, finalConfidence, promptConfig, ctx, 'low_confidence', config.leaseToken)
|
|
4427
4797
|
}
|
|
4428
4798
|
await emitAudit(ctx, 'agent.inference.completed', {
|
|
4429
4799
|
type: 'agent_message',
|
|
@@ -4442,10 +4812,28 @@ export async function runAgent(
|
|
|
4442
4812
|
confidence: final.data?.confidence
|
|
4443
4813
|
})
|
|
4444
4814
|
}
|
|
4815
|
+
await releaseIfOwned(
|
|
4816
|
+
state.status === 'complete' || state.status === 'awaiting_input' || state.status === 'failed'
|
|
4817
|
+
)
|
|
4445
4818
|
return lastMessage
|
|
4446
4819
|
}
|
|
4447
4820
|
|
|
4448
|
-
|
|
4821
|
+
// opts.deadlineMs bounds a complete-mode run inside a single request
|
|
4822
|
+
// (episode mode runs inline so the first turns execute at full
|
|
4823
|
+
// request CPU instead of on a CPU-throttled detached driver). On
|
|
4824
|
+
// expiry the episode keeps its lease and the caller's next step
|
|
4825
|
+
// request continues it — not a failure, so the max-turns tail below
|
|
4826
|
+
// is skipped.
|
|
4827
|
+
let budgetExceeded = false
|
|
4828
|
+
const loopStartTurn = state.turn_count
|
|
4829
|
+
for (let i = loopStartTurn; i < maxTurns; i++) {
|
|
4830
|
+
// Always run at least one step (progress guarantee), then stop
|
|
4831
|
+
// once the wall-clock budget is spent. state.turn_count advances
|
|
4832
|
+
// inside runAgentStep, so compare against the captured baseline.
|
|
4833
|
+
if (deadlineMs && i > loopStartTurn && Date.now() - startTime > deadlineMs) {
|
|
4834
|
+
budgetExceeded = true
|
|
4835
|
+
break
|
|
4836
|
+
}
|
|
4449
4837
|
const step = await runAgentStep(threadId, currentMessage, ctx, config)
|
|
4450
4838
|
lastMessage = step.message || lastMessage
|
|
4451
4839
|
state = step.state
|
|
@@ -4455,6 +4843,13 @@ export async function runAgent(
|
|
|
4455
4843
|
}
|
|
4456
4844
|
currentMessage = ''
|
|
4457
4845
|
}
|
|
4846
|
+
if (budgetExceeded &&
|
|
4847
|
+
state.status !== 'complete' && state.status !== 'awaiting_input' && state.status !== 'failed') {
|
|
4848
|
+
// Nonterminal partial run — keep the lease. The driver (episode
|
|
4849
|
+
// POST caller or a step-loop request) resumes with the same
|
|
4850
|
+
// driver_token, which renews the lease rather than double-driving.
|
|
4851
|
+
return lastMessage
|
|
4852
|
+
}
|
|
4458
4853
|
if (state.status === 'complete') {
|
|
4459
4854
|
const final = lastMessage || {}
|
|
4460
4855
|
// Same as the step-mode gate above: prefer the step's own computed
|
|
@@ -4466,7 +4861,7 @@ export async function runAgent(
|
|
|
4466
4861
|
if (!finalEscalated &&
|
|
4467
4862
|
finalConfidence < (promptConfig.confidence_threshold || 0) &&
|
|
4468
4863
|
finalEvidence !== 'evidence_found') {
|
|
4469
|
-
await handleEscalation(threadId, finalConfidence, promptConfig, ctx)
|
|
4864
|
+
await handleEscalation(threadId, finalConfidence, promptConfig, ctx, 'low_confidence', config.leaseToken)
|
|
4470
4865
|
}
|
|
4471
4866
|
await emitAudit(ctx, 'agent.inference.completed', {
|
|
4472
4867
|
type: 'agent_message',
|
|
@@ -4493,11 +4888,23 @@ export async function runAgent(
|
|
|
4493
4888
|
}, ctx, replyTypeSlug, 'external')
|
|
4494
4889
|
state.status = 'failed'
|
|
4495
4890
|
state.error_message = `Research did not complete after ${state.turn_count} turns`
|
|
4496
|
-
|
|
4891
|
+
state.completed_at = new Date().toISOString()
|
|
4892
|
+
await setResearchState(threadId, state, thread.data, config.leaseToken)
|
|
4497
4893
|
lastMessage = failMsg
|
|
4498
4894
|
}
|
|
4895
|
+
// The complete-mode tail is always terminal (complete /
|
|
4896
|
+
// awaiting_input / failed-after-max-turns).
|
|
4897
|
+
await releaseIfOwned(true)
|
|
4499
4898
|
return lastMessage
|
|
4500
4899
|
} catch (error: any) {
|
|
4900
|
+
if (error instanceof EpisodeLeaseLost) {
|
|
4901
|
+
// A takeover displaced this driver mid-run — quiet abort, not an
|
|
4902
|
+
// inference failure; the new owner writes the terminal message.
|
|
4903
|
+
console.warn(`[${ctx.requestId}] ${error.message} — aborting run; takeover owner continues`)
|
|
4904
|
+
await releaseIfOwned(true)
|
|
4905
|
+
return lastMessage
|
|
4906
|
+
}
|
|
4907
|
+
await releaseIfOwned(true)
|
|
4501
4908
|
await emitAudit(ctx, 'agent.inference.failed', {
|
|
4502
4909
|
type: 'agent_message',
|
|
4503
4910
|
id: 'failed',
|
|
@@ -4531,6 +4938,10 @@ export async function runAgent(
|
|
|
4531
4938
|
|
|
4532
4939
|
|
|
4533
4940
|
|
|
4941
|
+
|
|
4942
|
+
|
|
4943
|
+
|
|
4944
|
+
|
|
4534
4945
|
|
|
4535
4946
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:resolveAgentConfig:function" type="function" identity="resolveAgentConfig" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
4536
4947
|
/**
|
|
@@ -4730,6 +5141,10 @@ export async function resolveAgentConfig(
|
|
|
4730
5141
|
|
|
4731
5142
|
|
|
4732
5143
|
|
|
5144
|
+
|
|
5145
|
+
|
|
5146
|
+
|
|
5147
|
+
|
|
4733
5148
|
|
|
4734
5149
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeAgentInference:function" type="function" identity="executeAgentInference" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
4735
5150
|
/**
|
|
@@ -4836,6 +5251,10 @@ async function executeAgentInference(
|
|
|
4836
5251
|
|
|
4837
5252
|
|
|
4838
5253
|
|
|
5254
|
+
|
|
5255
|
+
|
|
5256
|
+
|
|
5257
|
+
|
|
4839
5258
|
|
|
4840
5259
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeCilChunkLookup:function" type="function" identity="executeCilChunkLookup" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
4841
5260
|
/**
|
|
@@ -4972,6 +5391,8 @@ async function executeCilChunkLookup(params: any, ctx: CoreContext, cilAccess: C
|
|
|
4972
5391
|
|
|
4973
5392
|
|
|
4974
5393
|
|
|
5394
|
+
|
|
5395
|
+
|
|
4975
5396
|
/**
|
|
4976
5397
|
* Fetches a document based on the provided document ID and context, ensuring it adheres to project restrictions if specified. It retrieves the document's content and metadata, handling various data structures and types.
|
|
4977
5398
|
*
|
|
@@ -4984,6 +5405,8 @@ async function executeCilChunkLookup(params: any, ctx: CoreContext, cilAccess: C
|
|
|
4984
5405
|
* @outputSpec Promise<any> — returns an object containing document metadata and content.
|
|
4985
5406
|
* @throws Error — document_id is required
|
|
4986
5407
|
Error — Document not found: {document_id}
|
|
5408
|
+
|
|
5409
|
+
|
|
4987
5410
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeCilFetchDocument:function" type="function" identity="executeCilFetchDocument" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
4988
5411
|
* @sideEffects Database read operations to fetch document data.
|
|
4989
5412
|
* @stability stable
|
|
@@ -5070,6 +5493,10 @@ async function executeCilFetchDocument(params: any, ctx: CoreContext, cilAccess:
|
|
|
5070
5493
|
|
|
5071
5494
|
|
|
5072
5495
|
|
|
5496
|
+
|
|
5497
|
+
|
|
5498
|
+
|
|
5499
|
+
|
|
5073
5500
|
|
|
5074
5501
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeCilGetContract:function" type="function" identity="executeCilGetContract" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5075
5502
|
/**
|
|
@@ -5158,6 +5585,10 @@ async function executeCilGetContract(params: any, ctx: CoreContext, cilAccess: C
|
|
|
5158
5585
|
|
|
5159
5586
|
|
|
5160
5587
|
|
|
5588
|
+
|
|
5589
|
+
|
|
5590
|
+
|
|
5591
|
+
|
|
5161
5592
|
|
|
5162
5593
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeCilBlastRadius:function" type="function" identity="executeCilBlastRadius" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5163
5594
|
/**
|
|
@@ -5299,6 +5730,10 @@ async function executeCilBlastRadius(params: any, ctx: CoreContext, cilAccess: C
|
|
|
5299
5730
|
|
|
5300
5731
|
|
|
5301
5732
|
|
|
5733
|
+
|
|
5734
|
+
|
|
5735
|
+
|
|
5736
|
+
|
|
5302
5737
|
|
|
5303
5738
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeCilHashValidate:function" type="function" identity="executeCilHashValidate" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5304
5739
|
/**
|
|
@@ -5378,6 +5813,10 @@ async function executeCilHashValidate(params: any, ctx: CoreContext, cilAccess:
|
|
|
5378
5813
|
|
|
5379
5814
|
|
|
5380
5815
|
|
|
5816
|
+
|
|
5817
|
+
|
|
5818
|
+
|
|
5819
|
+
|
|
5381
5820
|
|
|
5382
5821
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeCilProjectStatus:function" type="function" identity="executeCilProjectStatus" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5383
5822
|
/**
|
|
@@ -5486,6 +5925,10 @@ async function executeCilProjectStatus(params: any, ctx: CoreContext, cilAccess:
|
|
|
5486
5925
|
|
|
5487
5926
|
|
|
5488
5927
|
|
|
5928
|
+
|
|
5929
|
+
|
|
5930
|
+
|
|
5931
|
+
|
|
5489
5932
|
|
|
5490
5933
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeSearchLogs:function" type="function" identity="executeSearchLogs" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5491
5934
|
/**
|
|
@@ -5548,6 +5991,10 @@ async function executeSearchLogs(params: any, ctx: CoreContext): Promise<any> {
|
|
|
5548
5991
|
|
|
5549
5992
|
|
|
5550
5993
|
|
|
5994
|
+
|
|
5995
|
+
|
|
5996
|
+
|
|
5997
|
+
|
|
5551
5998
|
|
|
5552
5999
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:executeSearchObservability:function" type="function" identity="executeSearchObservability" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5553
6000
|
/**
|
|
@@ -5612,6 +6059,10 @@ async function executeSearchObservability(params: any, ctx: CoreContext): Promis
|
|
|
5612
6059
|
|
|
5613
6060
|
|
|
5614
6061
|
|
|
6062
|
+
|
|
6063
|
+
|
|
6064
|
+
|
|
6065
|
+
|
|
5615
6066
|
|
|
5616
6067
|
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:EPISODE_DRIVER_LEASE_MS:constant" type="constant" identity="EPISODE_DRIVER_LEASE_MS" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5617
6068
|
/**
|
|
@@ -5624,76 +6075,544 @@ async function executeSearchObservability(params: any, ctx: CoreContext): Promis
|
|
|
5624
6075
|
const EPISODE_DRIVER_LEASE_MS = 60000
|
|
5625
6076
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:EPISODE_DRIVER_LEASE_MS:constant"]
|
|
5626
6077
|
|
|
5627
|
-
|
|
6078
|
+
|
|
6079
|
+
|
|
6080
|
+
|
|
6081
|
+
|
|
6082
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:EPISODE_LEASE_TTL_SECONDS:constant" type="constant" identity="EPISODE_LEASE_TTL_SECONDS" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6083
|
+
/**
|
|
6084
|
+
* EPISODE_LEASE_TTL_SECONDS is the database-lease TTL a driver holds per
|
|
6085
|
+
* episode. Heartbeats extend it every turn; it must exceed the slowest
|
|
6086
|
+
* single turn (config resolution + buildContext + one inference call —
|
|
6087
|
+
* cold starts observed at ~45s) or a healthy driver would be fenced by
|
|
6088
|
+
* its own expiry.
|
|
6089
|
+
*
|
|
6090
|
+
* @sideEffects none
|
|
6091
|
+
* @stability stable
|
|
6092
|
+
* @cil-generated
|
|
6093
|
+
*/
|
|
6094
|
+
const EPISODE_LEASE_TTL_SECONDS = 90
|
|
6095
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:EPISODE_LEASE_TTL_SECONDS:constant"]
|
|
6096
|
+
|
|
6097
|
+
|
|
6098
|
+
|
|
6099
|
+
|
|
6100
|
+
|
|
6101
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:EPISODE_DETACHED_STALE_SECONDS:constant" type="constant" identity="EPISODE_DETACHED_STALE_SECONDS" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6102
|
+
/**
|
|
6103
|
+
* Heartbeat age (seconds) after which a 'detached:' lease holder counts as
|
|
6104
|
+
* stalled and may be stolen by a request-bound driver. A healthy detached
|
|
6105
|
+
* driver heartbeats at every turn boundary — the window tolerates one slow
|
|
6106
|
+
* turn (~13s inference + tools observed) without letting a routine episode
|
|
6107
|
+
* POST displace it mid-flight.
|
|
6108
|
+
*/
|
|
6109
|
+
const EPISODE_DETACHED_STALE_SECONDS = 30
|
|
6110
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:EPISODE_DETACHED_STALE_SECONDS:constant"]
|
|
6111
|
+
|
|
6112
|
+
|
|
6113
|
+
|
|
6114
|
+
|
|
6115
|
+
|
|
6116
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:EpisodeLeaseLost:class" type="class" identity="EpisodeLeaseLost" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6117
|
+
/**
|
|
6118
|
+
* EpisodeLeaseLost is thrown when a driver's lease_token no longer owns
|
|
6119
|
+
* the thread's episode_lease — i.e. an expired-lease takeover displaced
|
|
6120
|
+
* it mid-run. runAgent treats it as a quiet abort (the new owner
|
|
6121
|
+
* produces the terminal message), not an inference failure.
|
|
6122
|
+
*
|
|
6123
|
+
* @stability stable
|
|
6124
|
+
* @cil-generated
|
|
6125
|
+
*/
|
|
6126
|
+
class EpisodeLeaseLost extends Error {
|
|
6127
|
+
constructor(threadId: string) {
|
|
6128
|
+
super(`episode lease lost for thread ${threadId}`)
|
|
6129
|
+
this.name = 'EpisodeLeaseLost'
|
|
6130
|
+
}
|
|
6131
|
+
}
|
|
6132
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:EpisodeLeaseLost:class"]
|
|
6133
|
+
|
|
6134
|
+
|
|
6135
|
+
|
|
6136
|
+
|
|
6137
|
+
|
|
6138
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:acquireEpisodeLease:function" type="function" identity="acquireEpisodeLease" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6139
|
+
/**
|
|
6140
|
+
* Atomically claims the episode lease for a thread via the
|
|
6141
|
+
* acquire_episode_lease RPC (migration 063). Returns the RPC verdict:
|
|
6142
|
+
* 'claimed' with a lease_token the caller must present to
|
|
6143
|
+
* heartbeat/release, or 'held' with the live holder's driver_id.
|
|
6144
|
+
* Returns null when the RPC is unavailable (migration not applied) so
|
|
6145
|
+
* callers can fall back to the legacy research_state check.
|
|
6146
|
+
*
|
|
6147
|
+
* @inputSpec
|
|
6148
|
+
* threadId: string (required) — the thread whose episode is claimed.
|
|
6149
|
+
* accountId: string (required) — owning account for the lease row.
|
|
6150
|
+
* driverId: string (required) — the claiming driver's token.
|
|
6151
|
+
* ttlSeconds: number (optional) — lease TTL, default EPISODE_LEASE_TTL_SECONDS.
|
|
6152
|
+
* stealDetached: boolean (optional) — request-bound drivers pass true so a
|
|
6153
|
+
* live 'detached:'-prefixed holder (spawned background work) yields
|
|
6154
|
+
* immediately instead of forcing the caller to watch a throttled driver.
|
|
6155
|
+
* @outputSpec Promise<{outcome, lease_token?, holder_driver_id?} | null>
|
|
6156
|
+
* @sideEffects Inserts or updates public.episode_leases.
|
|
6157
|
+
* @stability stable
|
|
6158
|
+
* @calls adminDb.rpc('acquire_episode_lease')
|
|
6159
|
+
* @cil-generated
|
|
6160
|
+
*/
|
|
6161
|
+
export async function acquireEpisodeLease(
|
|
6162
|
+
threadId: string,
|
|
6163
|
+
accountId: string,
|
|
6164
|
+
driverId: string,
|
|
6165
|
+
ttlSeconds: number = EPISODE_LEASE_TTL_SECONDS,
|
|
6166
|
+
stealDetached = false,
|
|
6167
|
+
staleSeconds: number = EPISODE_DETACHED_STALE_SECONDS
|
|
6168
|
+
): Promise<{ outcome: 'claimed' | 'held'; lease_token?: string; holder_driver_id?: string } | null> {
|
|
6169
|
+
const { data, error } = await adminDb.rpc('acquire_episode_lease', {
|
|
6170
|
+
p_thread_id: threadId,
|
|
6171
|
+
p_account_id: accountId,
|
|
6172
|
+
p_driver_id: driverId,
|
|
6173
|
+
p_ttl_seconds: ttlSeconds,
|
|
6174
|
+
p_steal_detached: stealDetached,
|
|
6175
|
+
p_stale_seconds: staleSeconds
|
|
6176
|
+
})
|
|
6177
|
+
if (error) {
|
|
6178
|
+
// Fail-open: deployments that haven't applied 063 yet behave exactly
|
|
6179
|
+
// as before (legacy research_state check-then-write path).
|
|
6180
|
+
console.warn(`[episode-lease] acquire_episode_lease unavailable for ${threadId}: ${error.message}`)
|
|
6181
|
+
return null
|
|
6182
|
+
}
|
|
6183
|
+
if (!data || (data.outcome !== 'claimed' && data.outcome !== 'held')) {
|
|
6184
|
+
// Malformed verdict (unexpected RPC shape) — same fail-open path so
|
|
6185
|
+
// a surprising response never silently grants or refuses a driver.
|
|
6186
|
+
console.warn(`[episode-lease] acquire_episode_lease malformed response for ${threadId}`)
|
|
6187
|
+
return null
|
|
6188
|
+
}
|
|
6189
|
+
return data as { outcome: 'claimed' | 'held'; lease_token?: string; holder_driver_id?: string }
|
|
6190
|
+
}
|
|
6191
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:acquireEpisodeLease:function"]
|
|
6192
|
+
|
|
6193
|
+
|
|
6194
|
+
|
|
6195
|
+
|
|
6196
|
+
|
|
6197
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:heartbeatEpisodeLease:function" type="function" identity="heartbeatEpisodeLease" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6198
|
+
/**
|
|
6199
|
+
* Extends the held lease's TTL. Returns true when the token still owns
|
|
6200
|
+
* the lease, false when a takeover displaced this driver (the caller
|
|
6201
|
+
* must stop writing — that false is the write fence), and null when the
|
|
6202
|
+
* RPC is unavailable (fail-open so un-migrated deployments keep working).
|
|
6203
|
+
*
|
|
6204
|
+
* @inputSpec
|
|
6205
|
+
* threadId: string (required) — the leased thread.
|
|
6206
|
+
* leaseToken: string (required) — token returned by acquire_episode_lease.
|
|
6207
|
+
* @outputSpec Promise<boolean | null>
|
|
6208
|
+
* @sideEffects Updates public.episode_leases.expires_at.
|
|
6209
|
+
* @stability stable
|
|
6210
|
+
* @calls adminDb.rpc('heartbeat_episode_lease')
|
|
6211
|
+
* @cil-generated
|
|
6212
|
+
*/
|
|
6213
|
+
async function heartbeatEpisodeLease(
|
|
6214
|
+
threadId: string,
|
|
6215
|
+
leaseToken: string
|
|
6216
|
+
): Promise<boolean | null> {
|
|
6217
|
+
const { data, error } = await adminDb.rpc('heartbeat_episode_lease', {
|
|
6218
|
+
p_thread_id: threadId,
|
|
6219
|
+
p_lease_token: leaseToken,
|
|
6220
|
+
p_ttl_seconds: EPISODE_LEASE_TTL_SECONDS
|
|
6221
|
+
})
|
|
6222
|
+
if (error) {
|
|
6223
|
+
console.warn(`[episode-lease] heartbeat_episode_lease unavailable for ${threadId}: ${error.message}`)
|
|
6224
|
+
return null
|
|
6225
|
+
}
|
|
6226
|
+
// Only an explicit false is the takeover fence — malformed responses
|
|
6227
|
+
// fail open rather than aborting a healthy driver.
|
|
6228
|
+
return data === false ? false : data === true ? true : null
|
|
6229
|
+
}
|
|
6230
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:heartbeatEpisodeLease:function"]
|
|
6231
|
+
|
|
6232
|
+
|
|
6233
|
+
|
|
6234
|
+
|
|
6235
|
+
|
|
6236
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:mergeThreadData:function" type="function" identity="mergeThreadData" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6237
|
+
/**
|
|
6238
|
+
* Atomically shallow-merges a patch into threads.data via the
|
|
6239
|
+
* merge_thread_data RPC (migration 066). Unlike the read-modify-write
|
|
6240
|
+
* whole-blob update it replaces, the merge cannot resurrect stale keys:
|
|
6241
|
+
* a writer holding a pre-checkpoint snapshot can no longer clobber a
|
|
6242
|
+
* terminal research_state another writer persisted meanwhile.
|
|
6243
|
+
*
|
|
6244
|
+
* When leaseToken is provided the RPC drops the write if a
|
|
6245
|
+
* differently-tokened lease holds the thread — a displaced driver's
|
|
6246
|
+
* stale checkpoint becomes a no-op instead of overwriting the takeover
|
|
6247
|
+
* owner's terminal state.
|
|
6248
|
+
*
|
|
6249
|
+
* Returns true when written, false when fenced (or the thread is gone),
|
|
6250
|
+
* and null when the RPC is unavailable so callers can fall back to the
|
|
6251
|
+
* legacy whole-blob update on un-migrated deployments.
|
|
6252
|
+
*
|
|
6253
|
+
* @inputSpec
|
|
6254
|
+
* threadId: string (required) — the thread to patch.
|
|
6255
|
+
* patch: Record<string, any> (required) — top-level data keys to merge.
|
|
6256
|
+
* leaseToken: string (optional) — fence the write to this lease holder.
|
|
6257
|
+
* @outputSpec Promise<boolean | null>
|
|
6258
|
+
* @sideEffects Updates public.threads.data / updated_at.
|
|
6259
|
+
* @stability stable
|
|
6260
|
+
* @calls adminDb.rpc('merge_thread_data')
|
|
6261
|
+
* @cil-generated
|
|
6262
|
+
*/
|
|
6263
|
+
async function mergeThreadData(
|
|
6264
|
+
threadId: string,
|
|
6265
|
+
patch: Record<string, any>,
|
|
6266
|
+
leaseToken?: string
|
|
6267
|
+
): Promise<boolean | null> {
|
|
6268
|
+
const { data, error } = await adminDb.rpc('merge_thread_data', {
|
|
6269
|
+
p_thread_id: threadId,
|
|
6270
|
+
p_patch: patch,
|
|
6271
|
+
p_lease_token: leaseToken ?? null
|
|
6272
|
+
})
|
|
6273
|
+
if (error) {
|
|
6274
|
+
console.warn(`[episode-lease] merge_thread_data unavailable for ${threadId}: ${error.message}`)
|
|
6275
|
+
return null
|
|
6276
|
+
}
|
|
6277
|
+
return data === true ? true : data === false ? false : null
|
|
6278
|
+
}
|
|
6279
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:mergeThreadData:function"]
|
|
6280
|
+
|
|
6281
|
+
|
|
6282
|
+
|
|
6283
|
+
|
|
6284
|
+
|
|
6285
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:releaseEpisodeLease:function" type="function" identity="releaseEpisodeLease" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6286
|
+
/**
|
|
6287
|
+
* Releases the held lease on terminal state (complete/awaiting_input/
|
|
6288
|
+
* failed) or abort. Token-gated: a displaced driver's release returns
|
|
6289
|
+
* false, which is warn-only — the takeover owner controls the lease now.
|
|
6290
|
+
* Errors are swallowed (fail-open) so a missing migration never breaks
|
|
6291
|
+
* an otherwise successful episode.
|
|
6292
|
+
*
|
|
6293
|
+
* @inputSpec
|
|
6294
|
+
* threadId: string (required) — the leased thread.
|
|
6295
|
+
* leaseToken: string (required) — token returned by acquire_episode_lease.
|
|
6296
|
+
* @outputSpec Promise<void>
|
|
6297
|
+
* @sideEffects Deletes the public.episode_leases row when the token matches.
|
|
6298
|
+
* @stability stable
|
|
6299
|
+
* @calls adminDb.rpc('release_episode_lease')
|
|
6300
|
+
* @cil-generated
|
|
6301
|
+
*/
|
|
6302
|
+
async function releaseEpisodeLease(
|
|
6303
|
+
threadId: string,
|
|
6304
|
+
leaseToken: string
|
|
6305
|
+
): Promise<void> {
|
|
6306
|
+
const { data, error } = await adminDb.rpc('release_episode_lease', {
|
|
6307
|
+
p_thread_id: threadId,
|
|
6308
|
+
p_lease_token: leaseToken
|
|
6309
|
+
})
|
|
6310
|
+
if (error) {
|
|
6311
|
+
console.warn(`[episode-lease] release_episode_lease unavailable for ${threadId}: ${error.message}`)
|
|
6312
|
+
return
|
|
6313
|
+
}
|
|
6314
|
+
if (data !== true) {
|
|
6315
|
+
console.warn(`[episode-lease] release_episode_lease fenced for ${threadId} — lease already superseded`)
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:releaseEpisodeLease:function"]
|
|
6319
|
+
|
|
6320
|
+
|
|
6321
|
+
|
|
6322
|
+
|
|
6323
|
+
|
|
6324
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:isAnsweredQuestionReplay:function" type="function" identity="isAnsweredQuestionReplay" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6325
|
+
/**
|
|
6326
|
+
* Tells a re-delivered question from a genuine re-ask. A late claim can
|
|
6327
|
+
* carry the same userMessage text the just-finished episode already
|
|
6328
|
+
* answered — e.g. a queued triage kick claiming the freshly-released
|
|
6329
|
+
* lease after the request driver completed inline. Without the check the
|
|
6330
|
+
* claimant stamps fresh state and runs a whole second episode (the
|
|
6331
|
+
* duplicate escalate→final cycle observed in production).
|
|
6332
|
+
*
|
|
6333
|
+
* Verdict needs all three: terminal state (complete/failed), the carried
|
|
6334
|
+
* user_question text-identical to this message, and the latest inbound
|
|
6335
|
+
* row predating the terminal write — a same-text re-ask posts a NEWER
|
|
6336
|
+
* inbound, which is a new episode, not a replay.
|
|
6337
|
+
*
|
|
6338
|
+
* @inputSpec
|
|
6339
|
+
* threadId: string — the episode thread.
|
|
6340
|
+
* state: ResearchState | undefined — the persisted research_state.
|
|
6341
|
+
* userMessage: string — the message this claim/run carries.
|
|
6342
|
+
* @outputSpec Promise<boolean>
|
|
6343
|
+
* @sideEffects Reads messages (latest inbound) only when the cheap checks pass.
|
|
6344
|
+
* @stability stable
|
|
6345
|
+
* @calls adminDb.from('messages')
|
|
6346
|
+
* @cil-generated
|
|
6347
|
+
*/
|
|
6348
|
+
async function isAnsweredQuestionReplay(
|
|
6349
|
+
threadId: string,
|
|
6350
|
+
state: ResearchState | undefined,
|
|
6351
|
+
userMessage: string
|
|
6352
|
+
): Promise<boolean> {
|
|
6353
|
+
const question = (userMessage || '').trim()
|
|
6354
|
+
if (!question || !state) return false
|
|
6355
|
+
if (state.status !== 'complete' && state.status !== 'failed') return false
|
|
6356
|
+
if ((state.user_question || '').trim() !== question) return false
|
|
6357
|
+
const terminalAt = Date.parse(state.completed_at || state.driver_heartbeat_at || '')
|
|
6358
|
+
const { data: lastInbound } = await adminDb
|
|
6359
|
+
.from('messages')
|
|
6360
|
+
.select('created_at')
|
|
6361
|
+
.eq('thread_id', threadId)
|
|
6362
|
+
.eq('direction', 'inbound')
|
|
6363
|
+
.order('created_at', { ascending: false })
|
|
6364
|
+
.limit(1)
|
|
6365
|
+
const inboundAt = Date.parse((Array.isArray(lastInbound) ? lastInbound[0] : lastInbound)?.created_at || '')
|
|
6366
|
+
// Unbounded comparisons can't prove a replay — run the episode rather
|
|
6367
|
+
// than risk suppressing a real question. A 2s skew covers clock drift
|
|
6368
|
+
// between the inbound insert and the terminal write in the same txn.
|
|
6369
|
+
return Number.isFinite(inboundAt) && Number.isFinite(terminalAt) && inboundAt <= terminalAt + 2000
|
|
6370
|
+
}
|
|
6371
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:isAnsweredQuestionReplay:function"]
|
|
6372
|
+
|
|
6373
|
+
|
|
6374
|
+
|
|
6375
|
+
|
|
6376
|
+
|
|
6377
|
+
|
|
6378
|
+
|
|
6379
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:isHumanOwned:function" type="function" identity="isHumanOwned" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6380
|
+
// CIL[DOC-LEARNING topic="human_owner_gate" content="thread.data.owner:'human' is THE human/agent gate — while set, no agent episode may be claimed, narrated, or run. It is semantic ownership, orthogonal to the episode lease (who may write now). Both must hold for agent work: a free lease alone is not permission. The gate exists because serverless invocations freeze and resume into a changed world — a driver resuming after handoff found the lease free and re-escalated a thread waiting on a human (prod, Oct 2026)."]
|
|
6381
|
+
// CIL[DOC-NOTE type="caution" content="Core must NEVER gate on app-level item statuses (human_assigned, pending_resolution, human_action...) — core is domain-agnostic. Apps translate their workflow into the framework signal by stamping threads.data.owner:'human' themselves. The only core writer is handleEscalation."]
|
|
6382
|
+
/**
|
|
6383
|
+
* The human/agent ownership gate. `thread.data.owner === 'human'` means a
|
|
6384
|
+
* person owns this conversation and NO agent episode may be claimed,
|
|
6385
|
+
* narrated, or stepped — regardless of lease state. This is a semantic
|
|
6386
|
+
* gate, not a concurrency gate: the lease answers "who may write right
|
|
6387
|
+
* now", this answers "should any agent act at all".
|
|
6388
|
+
*
|
|
6389
|
+
* Writers:
|
|
6390
|
+
* - handleEscalation (escalate_to_human tool) — the agent hands off.
|
|
6391
|
+
* - App code — e.g. a human-reply or assign-to-me path stamping
|
|
6392
|
+
* owner:'human' on the thread when a person takes over.
|
|
6393
|
+
* Absence of the key means the thread is agent-eligible. The flag is never
|
|
6394
|
+
* auto-cleared: once a human owns a thread it stays human-owned (human-to-
|
|
6395
|
+
* human semantics); a new conversation means a new thread with fresh data.
|
|
6396
|
+
*
|
|
6397
|
+
* 'escalation_status: pending' is the pre-rename key — reads accept it so
|
|
6398
|
+
* rows written before the rename still gate correctly. New writes only
|
|
6399
|
+
* set 'owner'.
|
|
6400
|
+
*
|
|
6401
|
+
* @inputSpec
|
|
6402
|
+
* threadData: any — threads.data contents.
|
|
6403
|
+
* @outputSpec boolean — true when a human owns the thread.
|
|
6404
|
+
* @stability stable
|
|
6405
|
+
* @cil-generated
|
|
6406
|
+
*/
|
|
6407
|
+
function isHumanOwned(threadData: any): boolean {
|
|
6408
|
+
return threadData?.owner === 'human' || threadData?.escalation_status === 'pending'
|
|
6409
|
+
}
|
|
6410
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:isHumanOwned:function"]
|
|
6411
|
+
|
|
6412
|
+
|
|
6413
|
+
|
|
6414
|
+
|
|
6415
|
+
|
|
6416
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:claimAgentEpisode:function" type="function" identity="claimAgentEpisode" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
5628
6417
|
/**
|
|
5629
|
-
*
|
|
6418
|
+
* Atomically claims an episode for a driver: acquires the episode lease,
|
|
6419
|
+
* stamps research_state via the fenced merge RPC, and writes the intake
|
|
6420
|
+
* narration synchronously so the client has visible progress within the
|
|
6421
|
+
* request — not whenever a throttled detached driver gets CPU.
|
|
5630
6422
|
*
|
|
5631
|
-
* @macro
|
|
5632
|
-
* @micro
|
|
6423
|
+
* @macro Shared claim primitive for all episode entry paths.
|
|
6424
|
+
* @micro RPC claim → fenced research_state merge → synchronous intake narration → returns lease_token for the caller's run.
|
|
5633
6425
|
* @inputSpec
|
|
5634
6426
|
* threadId: string (required) — the unique identifier for the thread.
|
|
5635
6427
|
* userMessage: string (required) — the message from the user to the agent.
|
|
5636
6428
|
* ctx: CoreContext (required) — the context for the current request.
|
|
5637
6429
|
* leaseId: string (required) — the identifier for the lease holder.
|
|
5638
6430
|
* threadData: any (optional) — optional data associated with the thread.
|
|
5639
|
-
*
|
|
5640
|
-
*
|
|
6431
|
+
* opts.driverPrefix: string (optional) — e.g. 'detached:' marks stealable background drivers.
|
|
6432
|
+
* opts.stealDetached: boolean (optional) — take over a live 'detached:' holder.
|
|
6433
|
+
* @outputSpec Promise<{ claimed: boolean; driver_id?: string; lease_token?: string }> — claimed + the resolved driver id and lease token; on refusal driver_id is the live holder.
|
|
6434
|
+
* @sideEffects Writes episode_leases + threads.data.research_state + an intake 'plan' message.
|
|
5641
6435
|
* @stability stable
|
|
5642
|
-
* @calls
|
|
5643
|
-
* @errorHandling logs errors from the agent run without throwing.
|
|
6436
|
+
* @calls acquireEpisodeLease, mergeThreadData, saveMessage
|
|
5644
6437
|
* @cil-generated
|
|
5645
6438
|
*/
|
|
5646
|
-
export async function
|
|
6439
|
+
export async function claimAgentEpisode(
|
|
5647
6440
|
threadId: string,
|
|
5648
6441
|
userMessage: string,
|
|
5649
6442
|
ctx: CoreContext,
|
|
5650
6443
|
leaseId: string,
|
|
5651
|
-
threadData?: any
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
6444
|
+
threadData?: any,
|
|
6445
|
+
opts?: { driverPrefix?: string; stealDetached?: boolean }
|
|
6446
|
+
): Promise<{ claimed: boolean; driver_id?: string; lease_token?: string; already_done?: boolean }> {
|
|
6447
|
+
// CIL[DOC-NOTE type="ordering" content="The human-owner gate runs BEFORE the lease claim and from a fresh threads read — never the caller's threadData snapshot, which a frozen caller may have fetched before the handoff. Order matters: checking after claim would write an episode lease + narrate on a human-owned thread."]
|
|
6448
|
+
// Human-owner gate FIRST — before any lease or narration work, and always
|
|
6449
|
+
// from a fresh read rather than the caller's threadData snapshot: a
|
|
6450
|
+
// serverless invocation frozen between the caller's fetch and this claim
|
|
6451
|
+
// resumes into a world that may already be human-owned (observed in prod:
|
|
6452
|
+
// a frozen episode POST resumed after escalation, minted a second
|
|
6453
|
+
// episode, re-searched, and re-escalated a thread waiting on a human).
|
|
6454
|
+
const { data: ownerRow } = await adminDb
|
|
6455
|
+
.from('threads')
|
|
6456
|
+
.select('data')
|
|
6457
|
+
.eq('id', threadId)
|
|
6458
|
+
.single()
|
|
6459
|
+
if (isHumanOwned(ownerRow?.data)) {
|
|
6460
|
+
return { claimed: false, already_done: true }
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
// Atomic claim — the episode_leases row carries a heartbeat from
|
|
6464
|
+
// this point on, so stale-episode detectors see a live driver through
|
|
6465
|
+
// the whole kick prelude (rate limit, injection scan, config resolve)
|
|
6466
|
+
// instead of declaring the episode dead at ~20s of silence.
|
|
6467
|
+
const driverId = (opts?.driverPrefix || '') + leaseId
|
|
6468
|
+
const claim = ctx.accountId
|
|
6469
|
+
? await acquireEpisodeLease(threadId, ctx.accountId, driverId, EPISODE_LEASE_TTL_SECONDS, opts?.stealDetached ?? false)
|
|
6470
|
+
: null
|
|
6471
|
+
if (claim?.outcome === 'held') {
|
|
6472
|
+
return { claimed: false, driver_id: claim.holder_driver_id }
|
|
5661
6473
|
}
|
|
6474
|
+
|
|
6475
|
+
// Prefer the fresh owner-read over a caller-provided snapshot — it can
|
|
6476
|
+
// only be newer, and state/agent fields live on the same column.
|
|
6477
|
+
const data = ownerRow?.data || threadData || {}
|
|
5662
6478
|
const existingState = data?.research_state
|
|
5663
6479
|
const heartbeatAt = Date.parse(existingState?.driver_heartbeat_at || '')
|
|
5664
6480
|
const leaseHolder = existingState?.driver_id
|
|
5665
6481
|
const leaseLive =
|
|
5666
6482
|
!!leaseHolder &&
|
|
5667
|
-
leaseHolder !==
|
|
6483
|
+
leaseHolder !== driverId &&
|
|
5668
6484
|
!!existingState?.status &&
|
|
5669
6485
|
!['complete', 'awaiting_input', 'failed'].includes(existingState.status) &&
|
|
5670
6486
|
Number.isFinite(heartbeatAt) &&
|
|
5671
6487
|
Date.now() - heartbeatAt < EPISODE_DRIVER_LEASE_MS
|
|
5672
6488
|
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
6489
|
+
// claim === null means the RPC isn't deployed — fall back to the
|
|
6490
|
+
// legacy non-atomic check so un-migrated projects keep working.
|
|
6491
|
+
if (claim === null && leaseLive) {
|
|
6492
|
+
return { claimed: false, driver_id: leaseHolder }
|
|
6493
|
+
}
|
|
6494
|
+
// A claimed DB lease wins over a stale research_state mirror: another
|
|
6495
|
+
// caller can hold the JSON lease marker while the atomic lease is
|
|
6496
|
+
// free (e.g. it crashed before stamping) — the RPC verdict is
|
|
6497
|
+
// authoritative, this check only filters the legacy fallback above.
|
|
6498
|
+
|
|
6499
|
+
const leaseToken = claim?.lease_token
|
|
6500
|
+
|
|
6501
|
+
// Replay guard: a late claim carrying the question the episode already
|
|
6502
|
+
// answered (queued kick claiming the just-released lease) must not
|
|
6503
|
+
// stamp fresh state, narrate, or spawn a second episode — it releases
|
|
6504
|
+
// and reports already_done so the caller returns the existing result.
|
|
6505
|
+
if (await isAnsweredQuestionReplay(threadId, existingState, userMessage)) {
|
|
6506
|
+
if (leaseToken) await releaseEpisodeLease(threadId, leaseToken)
|
|
6507
|
+
return { claimed: false, already_done: true, driver_id: driverId }
|
|
6508
|
+
}
|
|
6509
|
+
const stampedState = {
|
|
6510
|
+
status: 'planning',
|
|
6511
|
+
turn_count: 0,
|
|
6512
|
+
max_turns: 5,
|
|
6513
|
+
gathered_results: [],
|
|
6514
|
+
...(existingState || {}),
|
|
6515
|
+
driver_id: driverId,
|
|
6516
|
+
driver_heartbeat_at: new Date().toISOString(),
|
|
6517
|
+
// The intake narration below was written for this driver — the
|
|
6518
|
+
// runAgent narration gate keys on driver_id so a same-driver rerun
|
|
6519
|
+
// never double-narrates and a different driver narrates fresh.
|
|
6520
|
+
intake_announced: true,
|
|
6521
|
+
}
|
|
6522
|
+
// Atomic merge — a whole-blob write here could clobber sibling keys a
|
|
6523
|
+
// concurrent writer set between our read and update.
|
|
6524
|
+
const merged = await mergeThreadData(threadId, { research_state: stampedState }, leaseToken)
|
|
6525
|
+
if (merged === false) {
|
|
6526
|
+
// Stolen between claim and stamp — the new owner drives now.
|
|
6527
|
+
return { claimed: false }
|
|
6528
|
+
}
|
|
6529
|
+
if (merged === null) {
|
|
6530
|
+
const { error: stampErr } = await adminDb
|
|
6531
|
+
.from('threads')
|
|
6532
|
+
.update({
|
|
6533
|
+
data: {
|
|
6534
|
+
...(data || {}),
|
|
6535
|
+
research_state: stampedState,
|
|
5688
6536
|
},
|
|
5689
|
-
}
|
|
5690
|
-
|
|
5691
|
-
.
|
|
6537
|
+
})
|
|
6538
|
+
.eq('id', threadId)
|
|
6539
|
+
if (stampErr) console.error(`claimAgentEpisode research_state stamp failed for ${threadId}:`, stampErr.message)
|
|
6540
|
+
}
|
|
6541
|
+
|
|
6542
|
+
// Intake narration is written synchronously inside the request — the
|
|
6543
|
+
// client sees progress within ~1s instead of waiting on a detached
|
|
6544
|
+
// driver whose first message can take 30s+ under CPU throttling.
|
|
6545
|
+
// Skipped when continuing an already-announced live episode (a steal
|
|
6546
|
+
// takeover re-claims mid-episode — re-narrating would double the
|
|
6547
|
+
// intake message). Failure is cosmetic: warn-only so narration never
|
|
6548
|
+
// fails the claim.
|
|
6549
|
+
const isContinuation = !!existingState
|
|
6550
|
+
&& !['complete', 'awaiting_input', 'failed'].includes(existingState.status)
|
|
6551
|
+
&& existingState.intake_announced === true
|
|
6552
|
+
if (!isContinuation) {
|
|
6553
|
+
try {
|
|
6554
|
+
await saveMessage(threadId, 'Reviewing your question…', 'plan', {
|
|
6555
|
+
turn_count: 0,
|
|
6556
|
+
phase: 'intake',
|
|
6557
|
+
agent_id: data?.agent_id || null,
|
|
6558
|
+
prompt_config_id: data?.prompt_config_id || null
|
|
6559
|
+
}, ctx, 'message', 'internal')
|
|
6560
|
+
} catch (narrErr: any) {
|
|
6561
|
+
console.warn(`[${ctx.requestId}] intake narration failed for thread ${threadId}:`, narrErr?.message)
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
return { claimed: true, driver_id: driverId, lease_token: leaseToken }
|
|
6565
|
+
}
|
|
6566
|
+
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:claimAgentEpisode:function"]
|
|
6567
|
+
|
|
6568
|
+
|
|
6569
|
+
|
|
6570
|
+
|
|
6571
|
+
|
|
6572
|
+
// CIL[BEGIN stable_id="~/.framework/functions/_shared/agent-runner.ts:kickAgentEpisode:function" type="function" identity="kickAgentEpisode" parent="~/.framework/functions/_shared/agent-runner.ts"]
|
|
6573
|
+
/**
|
|
6574
|
+
* Claims the episode and spawns a detached complete-mode driver — the
|
|
6575
|
+
* fallback path for episodes with no live client (ticket-creation kicks,
|
|
6576
|
+
* trigger-fired starts). The 'detached:' driver prefix lets a
|
|
6577
|
+
* request-bound caller steal the lease instantly rather than waiting on
|
|
6578
|
+
* CPU-throttled background work.
|
|
6579
|
+
*
|
|
6580
|
+
* @micro claimAgentEpisode(driverPrefix 'detached:') then void runAgent(complete).
|
|
6581
|
+
* @inputSpec
|
|
6582
|
+
* threadId: string (required) — the unique identifier for the thread.
|
|
6583
|
+
* userMessage: string (required) — the message from the user to the agent.
|
|
6584
|
+
* ctx: CoreContext (required) — the context for the current request.
|
|
6585
|
+
* leaseId: string (required) — the identifier for the lease holder.
|
|
6586
|
+
* threadData: any (optional) — optional data associated with the thread.
|
|
6587
|
+
* @outputSpec Promise<{ claimed: boolean; driver_id?: string }> — indicates whether the episode was claimed and optionally the driver ID.
|
|
6588
|
+
* @sideEffects Writes to the database to update the thread's state and initiates an agent run.
|
|
6589
|
+
* @stability stable
|
|
6590
|
+
* @calls claimAgentEpisode, runAgent
|
|
6591
|
+
* @errorHandling logs errors from the agent run without throwing.
|
|
6592
|
+
* @cil-generated
|
|
6593
|
+
*/
|
|
6594
|
+
export async function kickAgentEpisode(
|
|
6595
|
+
threadId: string,
|
|
6596
|
+
userMessage: string,
|
|
6597
|
+
ctx: CoreContext,
|
|
6598
|
+
leaseId: string,
|
|
6599
|
+
threadData?: any
|
|
6600
|
+
): Promise<{ claimed: boolean; driver_id?: string; already_done?: boolean }> {
|
|
6601
|
+
// 'detached:' marks the holder as spawned background work — a
|
|
6602
|
+
// request-bound driver claiming with steal_detached takes the lease
|
|
6603
|
+
// over immediately instead of waiting out a throttled driver, and the
|
|
6604
|
+
// detached run is fenced off at its next heartbeat.
|
|
6605
|
+
const claim = await claimAgentEpisode(threadId, userMessage, ctx, leaseId, threadData, { driverPrefix: 'detached:' })
|
|
6606
|
+
if (!claim.claimed) {
|
|
6607
|
+
return { claimed: false, driver_id: claim.driver_id, already_done: claim.already_done }
|
|
6608
|
+
}
|
|
5692
6609
|
|
|
5693
|
-
void runAgent(threadId, userMessage, ctx, 'complete',
|
|
6610
|
+
void runAgent(threadId, userMessage, ctx, 'complete', claim.driver_id, claim.lease_token).catch(err =>
|
|
5694
6611
|
console.error(`[${ctx.requestId}] detached episode run failed for thread ${threadId}:`, err)
|
|
5695
6612
|
)
|
|
5696
|
-
return { claimed: true }
|
|
6613
|
+
return { claimed: true, driver_id: claim.driver_id }
|
|
5697
6614
|
}
|
|
5698
6615
|
// CIL[END stable_id="~/.framework/functions/_shared/agent-runner.ts:kickAgentEpisode:function"]
|
|
5699
6616
|
|
|
6617
|
+
|
|
6618
|
+
|