chore: generate
This commit is contained in:
+35
-107
@@ -1205,13 +1205,9 @@
|
|||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1220,13 +1216,9 @@
|
|||||||
"table": "workspace"
|
"table": "workspace"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["active_account_id"],
|
||||||
"active_account_id"
|
|
||||||
],
|
|
||||||
"tableTo": "account",
|
"tableTo": "account",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "SET NULL",
|
"onDelete": "SET NULL",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1235,13 +1227,9 @@
|
|||||||
"table": "account_state"
|
"table": "account_state"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"tableTo": "event_sequence",
|
"tableTo": "event_sequence",
|
||||||
"columnsTo": [
|
"columnsTo": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1250,13 +1238,9 @@
|
|||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1265,13 +1249,9 @@
|
|||||||
"table": "permission"
|
"table": "permission"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1280,13 +1260,9 @@
|
|||||||
"table": "message"
|
"table": "message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["message_id"],
|
||||||
"message_id"
|
|
||||||
],
|
|
||||||
"tableTo": "message",
|
"tableTo": "message",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1295,13 +1271,9 @@
|
|||||||
"table": "part"
|
"table": "part"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1310,13 +1282,9 @@
|
|||||||
"table": "session_message"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1325,13 +1293,9 @@
|
|||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1340,13 +1304,9 @@
|
|||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1355,137 +1315,105 @@
|
|||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["email", "url"],
|
||||||
"email",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "control_account_pk",
|
"name": "control_account_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "control_account"
|
"table": "control_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id", "position"],
|
||||||
"session_id",
|
|
||||||
"position"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "todo_pk",
|
"name": "todo_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "workspace_pk",
|
"name": "workspace_pk",
|
||||||
"table": "workspace",
|
"table": "workspace",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["name"],
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "data_migration_pk",
|
"name": "data_migration_pk",
|
||||||
"table": "data_migration",
|
"table": "data_migration",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_state_pk",
|
"name": "account_state_pk",
|
||||||
"table": "account_state",
|
"table": "account_state",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_pk",
|
"name": "account_pk",
|
||||||
"table": "account",
|
"table": "account",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_sequence_pk",
|
"name": "event_sequence_pk",
|
||||||
"table": "event_sequence",
|
"table": "event_sequence",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_pk",
|
"name": "event_pk",
|
||||||
"table": "event",
|
"table": "event",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "permission_pk",
|
"name": "permission_pk",
|
||||||
"table": "permission",
|
"table": "permission",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_pk",
|
"name": "project_pk",
|
||||||
"table": "project",
|
"table": "project",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "message_pk",
|
"name": "message_pk",
|
||||||
"table": "message",
|
"table": "message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "part_pk",
|
"name": "part_pk",
|
||||||
"table": "part",
|
"table": "part",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_message_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_message",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_pk",
|
"name": "session_pk",
|
||||||
"table": "session",
|
"table": "session",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_share_pk",
|
"name": "session_share_pk",
|
||||||
"table": "session_share",
|
"table": "session_share",
|
||||||
|
|||||||
+42
-129
@@ -1333,13 +1333,9 @@
|
|||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1348,13 +1344,9 @@
|
|||||||
"table": "workspace"
|
"table": "workspace"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["active_account_id"],
|
||||||
"active_account_id"
|
|
||||||
],
|
|
||||||
"tableTo": "account",
|
"tableTo": "account",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "SET NULL",
|
"onDelete": "SET NULL",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1363,13 +1355,9 @@
|
|||||||
"table": "account_state"
|
"table": "account_state"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"tableTo": "event_sequence",
|
"tableTo": "event_sequence",
|
||||||
"columnsTo": [
|
"columnsTo": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1378,13 +1366,9 @@
|
|||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1393,13 +1377,9 @@
|
|||||||
"table": "permission"
|
"table": "permission"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1408,13 +1388,9 @@
|
|||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1423,13 +1399,9 @@
|
|||||||
"table": "message"
|
"table": "message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["message_id"],
|
||||||
"message_id"
|
|
||||||
],
|
|
||||||
"tableTo": "message",
|
"tableTo": "message",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1438,13 +1410,9 @@
|
|||||||
"table": "part"
|
"table": "part"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1453,13 +1421,9 @@
|
|||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1468,13 +1432,9 @@
|
|||||||
"table": "session_message"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id"],
|
||||||
"project_id"
|
|
||||||
],
|
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1483,13 +1443,9 @@
|
|||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1498,13 +1454,9 @@
|
|||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": [
|
"columnsTo": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1513,156 +1465,119 @@
|
|||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["email", "url"],
|
||||||
"email",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "control_account_pk",
|
"name": "control_account_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "control_account"
|
"table": "control_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["project_id", "directory"],
|
||||||
"project_id",
|
|
||||||
"directory"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_directory_pk",
|
"name": "project_directory_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id", "position"],
|
||||||
"session_id",
|
|
||||||
"position"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "todo_pk",
|
"name": "todo_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "workspace_pk",
|
"name": "workspace_pk",
|
||||||
"table": "workspace",
|
"table": "workspace",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["name"],
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "data_migration_pk",
|
"name": "data_migration_pk",
|
||||||
"table": "data_migration",
|
"table": "data_migration",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_state_pk",
|
"name": "account_state_pk",
|
||||||
"table": "account_state",
|
"table": "account_state",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_pk",
|
"name": "account_pk",
|
||||||
"table": "account",
|
"table": "account",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["aggregate_id"],
|
||||||
"aggregate_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_sequence_pk",
|
"name": "event_sequence_pk",
|
||||||
"table": "event_sequence",
|
"table": "event_sequence",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_pk",
|
"name": "event_pk",
|
||||||
"table": "event",
|
"table": "event",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "permission_pk",
|
"name": "permission_pk",
|
||||||
"table": "permission",
|
"table": "permission",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_pk",
|
"name": "project_pk",
|
||||||
"table": "project",
|
"table": "project",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "message_pk",
|
"name": "message_pk",
|
||||||
"table": "message",
|
"table": "message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "part_pk",
|
"name": "part_pk",
|
||||||
"table": "part",
|
"table": "part",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["seq"],
|
||||||
"seq"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_input_pk",
|
"name": "session_input_pk",
|
||||||
"table": "session_input",
|
"table": "session_input",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_message_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_message",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_pk",
|
"name": "session_pk",
|
||||||
"table": "session",
|
"table": "session",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["session_id"],
|
||||||
"session_id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_share_pk",
|
"name": "session_share_pk",
|
||||||
"table": "session_share",
|
"table": "session_share",
|
||||||
@@ -1943,9 +1858,7 @@
|
|||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": ["id"],
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_input_id_unique",
|
"name": "session_input_id_unique",
|
||||||
"entityType": "uniques",
|
"entityType": "uniques",
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export const Plugin = PluginV2.define({
|
|||||||
const transform = yield* skill.transform()
|
const transform = yield* skill.transform()
|
||||||
const entries = yield* config.entries()
|
const entries = yield* config.entries()
|
||||||
const items = entries.flatMap((entry) =>
|
const items = entries.flatMap((entry) =>
|
||||||
entry.type === "document" ? (entry.info.skills ?? []) : [path.join(entry.path, "skill"), path.join(entry.path, "skills")],
|
entry.type === "document"
|
||||||
|
? (entry.info.skills ?? [])
|
||||||
|
: [path.join(entry.path, "skill"), path.join(entry.path, "skills")],
|
||||||
)
|
)
|
||||||
|
|
||||||
yield* transform((editor) => {
|
yield* transform((editor) => {
|
||||||
|
|||||||
+6
-2
@@ -8,8 +8,12 @@ export default {
|
|||||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_idx\`;`)
|
||||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_idx\`;`)
|
||||||
yield* tx.run(`CREATE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
|
yield* tx.run(`CREATE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
|
||||||
yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
yield* tx.run(
|
||||||
yield* tx.run(`CREATE INDEX \`session_message_session_type_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`time_created\`,\`id\`);`)
|
`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`,
|
||||||
|
)
|
||||||
|
yield* tx.run(
|
||||||
|
`CREATE INDEX \`session_message_session_type_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`time_created\`,\`id\`);`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
} satisfies DatabaseMigration.Migration
|
} satisfies DatabaseMigration.Migration
|
||||||
|
|||||||
+11
-4
@@ -6,13 +6,20 @@ export default {
|
|||||||
up(tx) {
|
up(tx) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL DEFAULT 0;`)
|
yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL DEFAULT 0;`)
|
||||||
yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = COALESCE((SELECT \`seq\` + 1 FROM \`event\` WHERE \`event\`.\`id\` = \`session_message\`.\`id\`), 0);`)
|
yield* tx.run(
|
||||||
const unmatched = yield* tx.get<{ count: number }>(`SELECT COUNT(*) AS \`count\` FROM \`session_message\` WHERE \`seq\` = 0;`)
|
`UPDATE \`session_message\` SET \`seq\` = COALESCE((SELECT \`seq\` + 1 FROM \`event\` WHERE \`event\`.\`id\` = \`session_message\`.\`id\`), 0);`,
|
||||||
if ((unmatched?.count ?? 0) > 0) return yield* Effect.die("Cannot migrate session_message projections without matching durable events")
|
)
|
||||||
|
const unmatched = yield* tx.get<{ count: number }>(
|
||||||
|
`SELECT COUNT(*) AS \`count\` FROM \`session_message\` WHERE \`seq\` = 0;`,
|
||||||
|
)
|
||||||
|
if ((unmatched?.count ?? 0) > 0)
|
||||||
|
return yield* Effect.die("Cannot migrate session_message projections without matching durable events")
|
||||||
yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = \`seq\` - 1;`)
|
yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = \`seq\` - 1;`)
|
||||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`)
|
||||||
yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
|
yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
|
||||||
yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`)
|
yield* tx.run(
|
||||||
|
`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
} satisfies DatabaseMigration.Migration
|
} satisfies DatabaseMigration.Migration
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ export default {
|
|||||||
up(tx) {
|
up(tx) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_input_session_pending_seq_idx\`;`)
|
yield* tx.run(`DROP INDEX IF EXISTS \`session_input_session_pending_seq_idx\`;`)
|
||||||
yield* tx.run(`CREATE INDEX IF NOT EXISTS \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
|
yield* tx.run(
|
||||||
|
`CREATE INDEX IF NOT EXISTS \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`,
|
||||||
|
)
|
||||||
yield* tx.run(
|
yield* tx.run(
|
||||||
`CREATE INDEX IF NOT EXISTS \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`seq\`);`,
|
`CREATE INDEX IF NOT EXISTS \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`seq\`);`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ export interface KeyedMutex<in Key> {
|
|||||||
export const makeUnsafe = <Key>(): KeyedMutex<Key> => {
|
export const makeUnsafe = <Key>(): KeyedMutex<Key> => {
|
||||||
const locks = new Map<Key, { readonly semaphore: Semaphore.Semaphore; users: number }>()
|
const locks = new Map<Key, { readonly semaphore: Semaphore.Semaphore; users: number }>()
|
||||||
|
|
||||||
const withLock = (key: Key) => <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
const withLock =
|
||||||
|
(key: Key) =>
|
||||||
|
<A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||||
Effect.suspend(() => {
|
Effect.suspend(() => {
|
||||||
const current = locks.get(key)
|
const current = locks.get(key)
|
||||||
const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 }
|
const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 }
|
||||||
|
|||||||
+56
-13
@@ -146,7 +146,10 @@ export interface Interface {
|
|||||||
) => Effect.Effect<Payload<D>>
|
) => Effect.Effect<Payload<D>>
|
||||||
readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
|
readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
|
||||||
readonly all: () => Stream.Stream<Payload>
|
readonly all: () => Stream.Stream<Payload>
|
||||||
readonly aggregateEvents: (input: { readonly aggregateID: string; readonly after?: Cursor }) => Stream.Stream<CursorEvent>
|
readonly aggregateEvents: (input: {
|
||||||
|
readonly aggregateID: string
|
||||||
|
readonly after?: Cursor
|
||||||
|
}) => Stream.Stream<CursorEvent>
|
||||||
readonly sync: (handler: Sync) => Effect.Effect<Unsubscribe>
|
readonly sync: (handler: Sync) => Effect.Effect<Unsubscribe>
|
||||||
readonly listen: (listener: Listener) => Effect.Effect<Unsubscribe>
|
readonly listen: (listener: Listener) => Effect.Effect<Unsubscribe>
|
||||||
readonly beforeCommit: (guard: CommitGuard) => Effect.Effect<void>
|
readonly beforeCommit: (guard: CommitGuard) => Effect.Effect<void>
|
||||||
@@ -169,7 +172,8 @@ export interface LayerOptions {
|
|||||||
readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
|
readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const layerWith = (options?: LayerOptions) => Layer.effect(
|
export const layerWith = (options?: LayerOptions) =>
|
||||||
|
Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const all = yield* PubSub.unbounded<Payload>()
|
const all = yield* PubSub.unbounded<Payload>()
|
||||||
@@ -193,16 +197,23 @@ export const layerWith = (options?: LayerOptions) => Layer.effect(
|
|||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* PubSub.shutdown(all)
|
yield* PubSub.shutdown(all)
|
||||||
yield* Effect.forEach(synchronized.values(), (pubsubs) =>
|
yield* Effect.forEach(
|
||||||
Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
|
synchronized.values(),
|
||||||
{ discard: true })
|
(pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
|
||||||
|
{ discard: true },
|
||||||
|
)
|
||||||
yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true })
|
yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
function commitSyncEvent(
|
function commitSyncEvent(
|
||||||
event: Payload,
|
event: Payload,
|
||||||
input?: { readonly seq: number; readonly aggregateID: string; readonly ownerID?: string; readonly strictOwner?: boolean },
|
input?: {
|
||||||
|
readonly seq: number
|
||||||
|
readonly aggregateID: string
|
||||||
|
readonly ownerID?: string
|
||||||
|
readonly strictOwner?: boolean
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = registry.get(event.type)
|
const definition = registry.get(event.type)
|
||||||
@@ -274,13 +285,18 @@ export const layerWith = (options?: LayerOptions) => Layer.effect(
|
|||||||
for (const projector of list) {
|
for (const projector of list) {
|
||||||
yield* projector({ ...event, seq } as Payload)
|
yield* projector({ ...event, seq } as Payload)
|
||||||
}
|
}
|
||||||
const encoded = syncRegistry.get(versionedType(definition.type, sync.version))!.encode(event.data)
|
const encoded = syncRegistry
|
||||||
|
.get(versionedType(definition.type, sync.version))!
|
||||||
|
.encode(event.data)
|
||||||
yield* db
|
yield* db
|
||||||
.insert(EventSequenceTable)
|
.insert(EventSequenceTable)
|
||||||
.values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }])
|
.values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }])
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: EventSequenceTable.aggregate_id,
|
target: EventSequenceTable.aggregate_id,
|
||||||
set: { seq, ...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}) },
|
set: {
|
||||||
|
seq,
|
||||||
|
...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
@@ -356,7 +372,10 @@ export const layerWith = (options?: LayerOptions) => Layer.effect(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function replay(event: SerializedEvent, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }) {
|
function replay(
|
||||||
|
event: SerializedEvent,
|
||||||
|
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
||||||
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const definition = syncRegistry.get(event.type)
|
const definition = syncRegistry.get(event.type)
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
@@ -370,7 +389,12 @@ export const layerWith = (options?: LayerOptions) => Layer.effect(
|
|||||||
version: definition.sync.version,
|
version: definition.sync.version,
|
||||||
data: definition.decode(event.data),
|
data: definition.decode(event.data),
|
||||||
} as Payload
|
} as Payload
|
||||||
const committed = yield* commitSyncEvent(payload, { seq: event.seq, aggregateID: event.aggregateID, ownerID: options?.ownerID, strictOwner: options?.strictOwner })
|
const committed = yield* commitSyncEvent(payload, {
|
||||||
|
seq: event.seq,
|
||||||
|
aggregateID: event.aggregateID,
|
||||||
|
ownerID: options?.ownerID,
|
||||||
|
strictOwner: options?.strictOwner,
|
||||||
|
})
|
||||||
if (committed && options?.publish) {
|
if (committed && options?.publish) {
|
||||||
const published = { ...payload, seq: committed.seq }
|
const published = { ...payload, seq: committed.seq }
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
@@ -384,7 +408,10 @@ export const layerWith = (options?: LayerOptions) => Layer.effect(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function replayAll(events: SerializedEvent[], options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }) {
|
function replayAll(
|
||||||
|
events: SerializedEvent[],
|
||||||
|
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
||||||
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const source = events[0]?.aggregateID
|
const source = events[0]?.aggregateID
|
||||||
if (!source) return undefined
|
if (!source) return undefined
|
||||||
@@ -503,7 +530,10 @@ export const layerWith = (options?: LayerOptions) => Layer.effect(
|
|||||||
return subscription
|
return subscription
|
||||||
})
|
})
|
||||||
|
|
||||||
const streamEvents = (input: { readonly aggregateID: string; readonly after?: Cursor }): Stream.Stream<CursorEvent> =>
|
const streamEvents = (input: {
|
||||||
|
readonly aggregateID: string
|
||||||
|
readonly after?: Cursor
|
||||||
|
}): Stream.Stream<CursorEvent> =>
|
||||||
Stream.unwrap(
|
Stream.unwrap(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const synchronized = yield* subscribeSynchronized(input.aggregateID)
|
const synchronized = yield* subscribeSynchronized(input.aggregateID)
|
||||||
@@ -554,7 +584,20 @@ export const layerWith = (options?: LayerOptions) => Layer.effect(
|
|||||||
projectors.set(definition.type, list)
|
projectors.set(definition.type, list)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({ publish, subscribe, all: streamAll, aggregateEvents: streamEvents, sync, listen, beforeCommit, project, replay, replayAll, remove, claim })
|
return Service.of({
|
||||||
|
publish,
|
||||||
|
subscribe,
|
||||||
|
all: streamAll,
|
||||||
|
aggregateEvents: streamEvents,
|
||||||
|
sync,
|
||||||
|
listen,
|
||||||
|
beforeCommit,
|
||||||
|
project,
|
||||||
|
replay,
|
||||||
|
replayAll,
|
||||||
|
remove,
|
||||||
|
claim,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -52,17 +52,23 @@ export interface RemoveResult {
|
|||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
/** Create only while the planned target remains absent. */
|
/** Create only while the planned target remains absent. */
|
||||||
readonly create: (input: WriteInput) => Effect.Effect<WriteResult, TargetExistsError | LocationMutation.RevalidationError | FSUtil.Error>
|
readonly create: (
|
||||||
|
input: WriteInput,
|
||||||
|
) => Effect.Effect<WriteResult, TargetExistsError | LocationMutation.RevalidationError | FSUtil.Error>
|
||||||
/** Write after immediately revalidating the planned target. */
|
/** Write after immediately revalidating the planned target. */
|
||||||
readonly write: (input: WriteInput) => Effect.Effect<WriteResult, LocationMutation.RevalidationError | FSUtil.Error>
|
readonly write: (input: WriteInput) => Effect.Effect<WriteResult, LocationMutation.RevalidationError | FSUtil.Error>
|
||||||
/** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */
|
/** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */
|
||||||
readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect<WriteResult, LocationMutation.RevalidationError | FSUtil.Error>
|
readonly writeTextPreservingBom: (
|
||||||
|
input: TextWriteInput,
|
||||||
|
) => Effect.Effect<WriteResult, LocationMutation.RevalidationError | FSUtil.Error>
|
||||||
/** Commit only if an existing target still has the expected bytes. */
|
/** Commit only if an existing target still has the expected bytes. */
|
||||||
readonly writeIfUnchanged: (
|
readonly writeIfUnchanged: (
|
||||||
input: ConditionalWriteInput,
|
input: ConditionalWriteInput,
|
||||||
) => Effect.Effect<WriteResult, StaleContentError | LocationMutation.RevalidationError | FSUtil.Error>
|
) => Effect.Effect<WriteResult, StaleContentError | LocationMutation.RevalidationError | FSUtil.Error>
|
||||||
/** Remove after immediately revalidating the planned target. */
|
/** Remove after immediately revalidating the planned target. */
|
||||||
readonly remove: (input: RemoveInput) => Effect.Effect<RemoveResult, LocationMutation.RevalidationError | FSUtil.Error>
|
readonly remove: (
|
||||||
|
input: RemoveInput,
|
||||||
|
) => Effect.Effect<RemoveResult, LocationMutation.RevalidationError | FSUtil.Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileMutation") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileMutation") {}
|
||||||
@@ -92,12 +98,15 @@ export const layer = Layer.effect(
|
|||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const mutation = yield* LocationMutation.Service
|
const mutation = yield* LocationMutation.Service
|
||||||
const locks = KeyedMutex.makeUnsafe<string>()
|
const locks = KeyedMutex.makeUnsafe<string>()
|
||||||
const withTargetLock = (target: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
const withTargetLock =
|
||||||
|
(target: string) =>
|
||||||
|
<A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||||
locks.withLock(target)(Effect.uninterruptible(effect))
|
locks.withLock(target)(Effect.uninterruptible(effect))
|
||||||
|
|
||||||
const withValidatedTarget = (plan: LocationMutation.Plan) => <A, E, R>(
|
const withValidatedTarget =
|
||||||
commit: (target: LocationMutation.Target) => Effect.Effect<A, E, R>,
|
(plan: LocationMutation.Plan) =>
|
||||||
) => withTargetLock(plan.target.canonical)(mutation.revalidate(plan).pipe(Effect.flatMap(commit)))
|
<A, E, R>(commit: (target: LocationMutation.Target) => Effect.Effect<A, E, R>) =>
|
||||||
|
withTargetLock(plan.target.canonical)(mutation.revalidate(plan).pipe(Effect.flatMap(commit)))
|
||||||
|
|
||||||
const writeResult = (target: LocationMutation.Target, existed = target.exists): WriteResult => ({
|
const writeResult = (target: LocationMutation.Target, existed = target.exists): WriteResult => ({
|
||||||
operation: "write",
|
operation: "write",
|
||||||
@@ -138,7 +147,8 @@ export const layer = Layer.effect(
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (target.exists) return yield* new TargetExistsError({ path: target.canonical })
|
if (target.exists) return yield* new TargetExistsError({ path: target.canonical })
|
||||||
yield* fs.ensureDir(dirname(target.canonical))
|
yield* fs.ensureDir(dirname(target.canonical))
|
||||||
if (typeof input.content === "string") yield* fs.writeFileString(target.canonical, input.content, { flag: "wx" })
|
if (typeof input.content === "string")
|
||||||
|
yield* fs.writeFileString(target.canonical, input.content, { flag: "wx" })
|
||||||
else yield* fs.writeFile(target.canonical, input.content, { flag: "wx" })
|
else yield* fs.writeFile(target.canonical, input.content, { flag: "wx" })
|
||||||
return writeResult(target, false)
|
return writeResult(target, false)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -447,7 +447,11 @@ export const layer = Layer.effect(
|
|||||||
const canonical = yield* fs.realPath(target.absolute).pipe(Effect.orDie)
|
const canonical = yield* fs.realPath(target.absolute).pipe(Effect.orDie)
|
||||||
if (canonical !== target.real) return yield* Effect.die(new Error("Search root changed after approval"))
|
if (canonical !== target.real) return yield* Effect.die(new Error("Search root changed after approval"))
|
||||||
const info = yield* fs.stat(canonical).pipe(Effect.orDie)
|
const info = yield* fs.stat(canonical).pipe(Effect.orDie)
|
||||||
if (info.type !== (target.type === "file" ? "File" : "Directory") || info.dev !== target.dev || Option.getOrUndefined(info.ino) !== target.ino)
|
if (
|
||||||
|
info.type !== (target.type === "file" ? "File" : "Directory") ||
|
||||||
|
info.dev !== target.dev ||
|
||||||
|
Option.getOrUndefined(info.ino) !== target.ino
|
||||||
|
)
|
||||||
return yield* Effect.die(new Error("Search root identity changed after approval"))
|
return yield* Effect.die(new Error("Search root identity changed after approval"))
|
||||||
return target
|
return target
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -241,7 +241,13 @@ export const layer = Layer.effect(
|
|||||||
const directory =
|
const directory =
|
||||||
boundary.exists && boundary.type === "Directory" ? boundary.canonical : boundary.authority.canonical
|
boundary.exists && boundary.type === "Directory" ? boundary.canonical : boundary.authority.canonical
|
||||||
const resource = slash(path.join(directory, "*"))
|
const resource = slash(path.join(directory, "*"))
|
||||||
return { action: "external_directory" as const, directory, resource, save: resource, authority: boundary.authority }
|
return {
|
||||||
|
action: "external_directory" as const,
|
||||||
|
directory,
|
||||||
|
resource,
|
||||||
|
save: resource,
|
||||||
|
authority: boundary.authority,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {
|
const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ export class GrepResult extends Schema.Class<GrepResult>("LocationSearch.GrepRes
|
|||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly files: (input: FilesInput, root?: FileSystem.RootTarget) => Effect.Effect<FilesResult, Ripgrep.Error>
|
readonly files: (input: FilesInput, root?: FileSystem.RootTarget) => Effect.Effect<FilesResult, Ripgrep.Error>
|
||||||
readonly grep: (input: GrepInput, root?: FileSystem.RootTarget) => Effect.Effect<GrepResult, Ripgrep.Error | Ripgrep.InvalidPatternError>
|
readonly grep: (
|
||||||
|
input: GrepInput,
|
||||||
|
root?: FileSystem.RootTarget,
|
||||||
|
) => Effect.Effect<GrepResult, Ripgrep.Error | Ripgrep.InvalidPatternError>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/LocationSearch") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/LocationSearch") {}
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ export * as Patch from "./patch"
|
|||||||
export type Hunk =
|
export type Hunk =
|
||||||
| { readonly type: "add"; readonly path: string; readonly contents: string }
|
| { readonly type: "add"; readonly path: string; readonly contents: string }
|
||||||
| { readonly type: "delete"; readonly path: string }
|
| { readonly type: "delete"; readonly path: string }
|
||||||
| { readonly type: "update"; readonly path: string; readonly movePath?: string; readonly chunks: ReadonlyArray<UpdateFileChunk> }
|
| {
|
||||||
|
readonly type: "update"
|
||||||
|
readonly path: string
|
||||||
|
readonly movePath?: string
|
||||||
|
readonly chunks: ReadonlyArray<UpdateFileChunk>
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateFileChunk {
|
export interface UpdateFileChunk {
|
||||||
readonly oldLines: ReadonlyArray<string>
|
readonly oldLines: ReadonlyArray<string>
|
||||||
@@ -166,7 +171,12 @@ function seek(lines: ReadonlyArray<string>, pattern: ReadonlyArray<string>, star
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function matches(lines: ReadonlyArray<string>, pattern: ReadonlyArray<string>, offset: number, compare: (left: string, right: string) => boolean) {
|
function matches(
|
||||||
|
lines: ReadonlyArray<string>,
|
||||||
|
pattern: ReadonlyArray<string>,
|
||||||
|
offset: number,
|
||||||
|
compare: (left: string, right: string) => boolean,
|
||||||
|
) {
|
||||||
return pattern.every((line, index) => compare(lines[offset + index]!, line))
|
return pattern.every((line, index) => compare(lines[offset + index]!, line))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +184,14 @@ const exact = (left: string, right: string) => left === right
|
|||||||
const rstrip = (left: string, right: string) => left.trimEnd() === right.trimEnd()
|
const rstrip = (left: string, right: string) => left.trimEnd() === right.trimEnd()
|
||||||
const trim = (left: string, right: string) => left.trim() === right.trim()
|
const trim = (left: string, right: string) => left.trim() === right.trim()
|
||||||
const normalized = (left: string, right: string) => normalize(left.trim()) === normalize(right.trim())
|
const normalized = (left: string, right: string) => normalize(left.trim()) === normalize(right.trim())
|
||||||
const normalize = (value: string) => value.replace(/[‘’‚‛]/g, "'").replace(/[“”„‟]/g, '"').replace(/[‐‑‒–—―]/g, "-").replace(/…/g, "...").replace(/ /g, " ")
|
const normalize = (value: string) =>
|
||||||
const splitBom = (text: string) => text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text }
|
value
|
||||||
const stripHeredoc = (input: string) => input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input
|
.replace(/[‘’‚‛]/g, "'")
|
||||||
|
.replace(/[“”„‟]/g, '"')
|
||||||
|
.replace(/[‐‑‒–—―]/g, "-")
|
||||||
|
.replace(/…/g, "...")
|
||||||
|
.replace(/ /g, " ")
|
||||||
|
const splitBom = (text: string) =>
|
||||||
|
text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text }
|
||||||
|
const stripHeredoc = (input: string) =>
|
||||||
|
input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ export const layer = Layer.effect(
|
|||||||
const item = { request, deferred }
|
const item = { request, deferred }
|
||||||
if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`)
|
if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`)
|
||||||
pending.set(request.id, item)
|
pending.set(request.id, item)
|
||||||
yield* events.publish(Event.Asked, request).pipe(
|
yield* events
|
||||||
EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))),
|
.publish(Event.Asked, request)
|
||||||
)
|
.pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))))
|
||||||
return item
|
return item
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -236,7 +236,9 @@ export const layer = Layer.effect(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => EffectRuntime.uninterruptible(EffectRuntime.gen(function* () {
|
const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) =>
|
||||||
|
EffectRuntime.uninterruptible(
|
||||||
|
EffectRuntime.gen(function* () {
|
||||||
const existing = pending.get(input.requestID)
|
const existing = pending.get(input.requestID)
|
||||||
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
||||||
yield* events.publish(Event.Replied, {
|
yield* events.publish(Event.Replied, {
|
||||||
@@ -298,7 +300,9 @@ export const layer = Layer.effect(
|
|||||||
yield* Deferred.succeed(item.deferred, undefined)
|
yield* Deferred.succeed(item.deferred, undefined)
|
||||||
pending.delete(id)
|
pending.delete(id)
|
||||||
}
|
}
|
||||||
})))
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const list = EffectRuntime.fn("PermissionV2.list")(function* () {
|
const list = EffectRuntime.fn("PermissionV2.list")(function* () {
|
||||||
return Array.from(pending.values(), (item) => item.request)
|
return Array.from(pending.values(), (item) => item.request)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const RawMatch = Schema.Struct({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Match = typeof RawMatch.Type["data"]
|
export type Match = (typeof RawMatch.Type)["data"]
|
||||||
|
|
||||||
export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", {
|
export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", {
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
@@ -77,7 +77,8 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||||||
|
|
||||||
const failure = (message: string, cause?: unknown) => new Error({ message, cause })
|
const failure = (message: string, cause?: unknown) => new Error({ message, cause })
|
||||||
|
|
||||||
const isInvalidPattern = (stderr: string) => stderr.includes("regex parse error") || stderr.includes("error parsing regex")
|
const isInvalidPattern = (stderr: string) =>
|
||||||
|
stderr.includes("regex parse error") || stderr.includes("error parsing regex")
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
@@ -126,7 +127,13 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const abortable = input.signal ? program.pipe(Effect.raceFirst(waitForAbort(input.signal))) : program
|
const abortable = input.signal ? program.pipe(Effect.raceFirst(waitForAbort(input.signal))) : program
|
||||||
return abortable.pipe(Effect.mapError((cause) => cause instanceof Error || cause instanceof InvalidPatternError ? cause : failure("ripgrep execution failed", cause)))
|
return abortable.pipe(
|
||||||
|
Effect.mapError((cause) =>
|
||||||
|
cause instanceof Error || cause instanceof InvalidPatternError
|
||||||
|
? cause
|
||||||
|
: failure("ripgrep execution failed", cause),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
@@ -143,9 +150,7 @@ export const layer = Layer.effect(
|
|||||||
".",
|
".",
|
||||||
],
|
],
|
||||||
parse: (line) => Effect.succeed(line.replace(/^\.\//, "")),
|
parse: (line) => Effect.succeed(line.replace(/^\.\//, "")),
|
||||||
}).pipe(
|
}).pipe(Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause)))),
|
||||||
Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))),
|
|
||||||
),
|
|
||||||
grep: (input) =>
|
grep: (input) =>
|
||||||
run<Match>({
|
run<Match>({
|
||||||
...input,
|
...input,
|
||||||
@@ -167,11 +172,16 @@ export const layer = Layer.effect(
|
|||||||
: Effect.try({
|
: Effect.try({
|
||||||
try: () => JSON.parse(line) as unknown,
|
try: () => JSON.parse(line) as unknown,
|
||||||
catch: (cause) => failure("Invalid ripgrep JSON output", cause),
|
catch: (cause) => failure("Invalid ripgrep JSON output", cause),
|
||||||
})).pipe(
|
})
|
||||||
|
).pipe(
|
||||||
Effect.flatMap((json) => {
|
Effect.flatMap((json) => {
|
||||||
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match") return Effect.succeed(undefined)
|
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match")
|
||||||
|
return Effect.succeed(undefined)
|
||||||
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
|
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
|
||||||
Effect.map((match) => ({ ...match.data, submatches: match.data.submatches.slice(0, MAX_SUBMATCHES) })),
|
Effect.map((match) => ({
|
||||||
|
...match.data,
|
||||||
|
submatches: match.data.submatches.slice(0, MAX_SUBMATCHES),
|
||||||
|
})),
|
||||||
Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
|
Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ export type ExternalID = {
|
|||||||
readonly key: string
|
readonly key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const externalID = (prefix: string, input: ExternalID) => `${prefix}_${Hash.sha256(JSON.stringify([input.namespace, input.key]))}`
|
export const externalID = (prefix: string, input: ExternalID) =>
|
||||||
|
`${prefix}_${Hash.sha256(JSON.stringify([input.namespace, input.key]))}`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integer greater than zero.
|
* Integer greater than zero.
|
||||||
|
|||||||
@@ -132,8 +132,14 @@ export interface Interface {
|
|||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
after?: EventV2.Cursor
|
after?: EventV2.Cursor
|
||||||
}) => Stream.Stream<EventV2.CursorEvent<SessionEvent.DurableEvent>, NotFoundError>
|
}) => Stream.Stream<EventV2.CursorEvent<SessionEvent.DurableEvent>, NotFoundError>
|
||||||
readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect<void, OperationUnavailableError>
|
readonly switchAgent: (input: {
|
||||||
readonly switchModel: (input: { sessionID: SessionSchema.ID; model: ModelV2.Ref }) => Effect.Effect<void, OperationUnavailableError>
|
sessionID: SessionSchema.ID
|
||||||
|
agent: string
|
||||||
|
}) => Effect.Effect<void, OperationUnavailableError>
|
||||||
|
readonly switchModel: (input: {
|
||||||
|
sessionID: SessionSchema.ID
|
||||||
|
model: ModelV2.Ref
|
||||||
|
}) => Effect.Effect<void, OperationUnavailableError>
|
||||||
readonly prompt: (input: {
|
readonly prompt: (input: {
|
||||||
id?: SessionMessage.ID
|
id?: SessionMessage.ID
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
@@ -318,7 +324,9 @@ export const layer = Layer.effect(
|
|||||||
? yield* db
|
? yield* db
|
||||||
.select({ seq: SessionMessageTable.seq })
|
.select({ seq: SessionMessageTable.seq })
|
||||||
.from(SessionMessageTable)
|
.from(SessionMessageTable)
|
||||||
.where(and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)))
|
.where(
|
||||||
|
and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)),
|
||||||
|
)
|
||||||
.get()
|
.get()
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ export const equivalent = (
|
|||||||
) => input.delivery === expected.delivery && matchesPrompt(input, expected)
|
) => input.delivery === expected.delivery && matchesPrompt(input, expected)
|
||||||
|
|
||||||
const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionSchema.ID; readonly prompt: Prompt }) =>
|
const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionSchema.ID; readonly prompt: Prompt }) =>
|
||||||
input.sessionID === expected.sessionID && JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
|
input.sessionID === expected.sessionID &&
|
||||||
|
JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
|
||||||
|
|
||||||
export const guardReservedID = Effect.fn("SessionInput.guardReservedID")(function* (
|
export const guardReservedID = Effect.fn("SessionInput.guardReservedID")(function* (
|
||||||
db: DatabaseService,
|
db: DatabaseService,
|
||||||
|
|||||||
@@ -318,14 +318,16 @@ export const layer = Layer.effectDiscard(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.AgentSwitched, (event) =>
|
yield* events.project(SessionEvent.AgentSwitched, (event) =>
|
||||||
db.update(SessionTable)
|
db
|
||||||
|
.update(SessionTable)
|
||||||
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||||
.where(eq(SessionTable.id, event.data.sessionID))
|
.where(eq(SessionTable.id, event.data.sessionID))
|
||||||
.run()
|
.run()
|
||||||
.pipe(Effect.orDie, Effect.andThen(run(db, event))),
|
.pipe(Effect.orDie, Effect.andThen(run(db, event))),
|
||||||
)
|
)
|
||||||
yield* events.project(SessionEvent.ModelSwitched, (event) =>
|
yield* events.project(SessionEvent.ModelSwitched, (event) =>
|
||||||
db.update(SessionTable)
|
db
|
||||||
|
.update(SessionTable)
|
||||||
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
|
||||||
.where(eq(SessionTable.id, event.data.sessionID))
|
.where(eq(SessionTable.id, event.data.sessionID))
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { ToolOutput as LLMToolOutput, type LLMEvent, type ProviderMetadata, type ToolOutput as LLMToolOutputType, type ToolResultValue, type Usage } from "@opencode-ai/llm"
|
import {
|
||||||
|
ToolOutput as LLMToolOutput,
|
||||||
|
type LLMEvent,
|
||||||
|
type ProviderMetadata,
|
||||||
|
type ToolOutput as LLMToolOutputType,
|
||||||
|
type ToolResultValue,
|
||||||
|
type Usage,
|
||||||
|
} from "@opencode-ai/llm"
|
||||||
import { DateTime, Effect } from "effect"
|
import { DateTime, Effect } from "effect"
|
||||||
import { EventV2 } from "../../event"
|
import { EventV2 } from "../../event"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
@@ -52,7 +59,15 @@ const settledOutput = (value: LLMToolOutputType | undefined, result: ToolResultV
|
|||||||
export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => {
|
export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => {
|
||||||
const tools = new Map<
|
const tools = new Map<
|
||||||
string,
|
string,
|
||||||
{ readonly assistantMessageID: EventV2.ID; readonly name: string; inputEnded: boolean; called: boolean; settled: boolean; providerExecuted: boolean; providerMetadata?: ProviderMetadata }
|
{
|
||||||
|
readonly assistantMessageID: EventV2.ID
|
||||||
|
readonly name: string
|
||||||
|
inputEnded: boolean
|
||||||
|
called: boolean
|
||||||
|
settled: boolean
|
||||||
|
providerExecuted: boolean
|
||||||
|
providerMetadata?: ProviderMetadata
|
||||||
|
}
|
||||||
>()
|
>()
|
||||||
const timestamp = DateTime.now
|
const timestamp = DateTime.now
|
||||||
let assistantMessageID: EventV2.ID | undefined
|
let assistantMessageID: EventV2.ID | undefined
|
||||||
@@ -60,14 +75,19 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
|
|
||||||
const startAssistant = Effect.fnUntraced(function* () {
|
const startAssistant = Effect.fnUntraced(function* () {
|
||||||
if (assistantMessageID !== undefined) return assistantMessageID
|
if (assistantMessageID !== undefined) return assistantMessageID
|
||||||
assistantMessageID = (yield* events.publish(SessionEvent.Step.Started, { ...input, timestamp: yield* timestamp })).id
|
assistantMessageID = (yield* events.publish(SessionEvent.Step.Started, { ...input, timestamp: yield* timestamp }))
|
||||||
|
.id
|
||||||
return assistantMessageID
|
return assistantMessageID
|
||||||
})
|
})
|
||||||
const currentAssistantMessageID = () => assistantMessageID === undefined
|
const currentAssistantMessageID = () =>
|
||||||
|
assistantMessageID === undefined
|
||||||
? Effect.die("Tool event before assistant step start")
|
? Effect.die("Tool event before assistant step start")
|
||||||
: Effect.succeed(assistantMessageID)
|
: Effect.succeed(assistantMessageID)
|
||||||
|
|
||||||
const fragments = (name: string, ended: (id: string, value: string, providerMetadata?: ProviderMetadata) => Effect.Effect<void>) => {
|
const fragments = (
|
||||||
|
name: string,
|
||||||
|
ended: (id: string, value: string, providerMetadata?: ProviderMetadata) => Effect.Effect<void>,
|
||||||
|
) => {
|
||||||
const chunks = new Map<string, string[]>()
|
const chunks = new Map<string, string[]>()
|
||||||
const start = (id: string) =>
|
const start = (id: string) =>
|
||||||
Effect.suspend(() => {
|
Effect.suspend(() => {
|
||||||
@@ -139,7 +159,14 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
|
const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
|
||||||
if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`)
|
if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`)
|
||||||
const assistantMessageID = yield* currentAssistantMessageID()
|
const assistantMessageID = yield* currentAssistantMessageID()
|
||||||
tools.set(event.id, { assistantMessageID, name: event.name, inputEnded: false, called: false, settled: false, providerExecuted: false })
|
tools.set(event.id, {
|
||||||
|
assistantMessageID,
|
||||||
|
name: event.name,
|
||||||
|
inputEnded: false,
|
||||||
|
called: false,
|
||||||
|
settled: false,
|
||||||
|
providerExecuted: false,
|
||||||
|
})
|
||||||
yield* toolInput.start(event.id)
|
yield* toolInput.start(event.id)
|
||||||
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -153,7 +180,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
|
const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
|
||||||
const tool = tools.get(event.id)
|
const tool = tools.get(event.id)
|
||||||
if (!tool) return yield* Effect.die(`Tool input end before start: ${event.id}`)
|
if (!tool) return yield* Effect.die(`Tool input end before start: ${event.id}`)
|
||||||
if (tool.name !== event.name) return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
if (tool.name !== event.name)
|
||||||
|
return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||||
if (tool.inputEnded) return yield* Effect.die(`Duplicate tool input end: ${event.id}`)
|
if (tool.inputEnded) return yield* Effect.die(`Duplicate tool input end: ${event.id}`)
|
||||||
yield* toolInput.end(event.id)
|
yield* toolInput.end(event.id)
|
||||||
})
|
})
|
||||||
@@ -190,7 +218,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
return
|
return
|
||||||
case "text-start":
|
case "text-start":
|
||||||
yield* text.start(event.id)
|
yield* text.start(event.id)
|
||||||
yield* events.publish(SessionEvent.Text.Started, { sessionID: input.sessionID, timestamp: yield* timestamp, textID: event.id })
|
yield* events.publish(SessionEvent.Text.Started, {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
timestamp: yield* timestamp,
|
||||||
|
textID: event.id,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
case "text-delta":
|
case "text-delta":
|
||||||
yield* text.append(event.id, event.text)
|
yield* text.append(event.id, event.text)
|
||||||
@@ -231,7 +263,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
case "tool-input-delta": {
|
case "tool-input-delta": {
|
||||||
const tool = tools.get(event.id)
|
const tool = tools.get(event.id)
|
||||||
if (!tool) return yield* Effect.die(`Tool input delta before start: ${event.id}`)
|
if (!tool) return yield* Effect.die(`Tool input delta before start: ${event.id}`)
|
||||||
if (tool.name !== event.name) return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
if (tool.name !== event.name)
|
||||||
|
return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||||
if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`)
|
if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`)
|
||||||
yield* toolInput.append(event.id, event.text)
|
yield* toolInput.append(event.id, event.text)
|
||||||
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
||||||
@@ -250,7 +283,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
if (!tools.has(event.id)) yield* startToolInput(event)
|
if (!tools.has(event.id)) yield* startToolInput(event)
|
||||||
const tool = tools.get(event.id)!
|
const tool = tools.get(event.id)!
|
||||||
if (!tool.inputEnded) yield* endToolInput(event)
|
if (!tool.inputEnded) yield* endToolInput(event)
|
||||||
if (tool.name !== event.name) return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
if (tool.name !== event.name)
|
||||||
|
return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||||
if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`)
|
if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`)
|
||||||
tool.called = true
|
tool.called = true
|
||||||
tool.providerExecuted = event.providerExecuted === true
|
tool.providerExecuted = event.providerExecuted === true
|
||||||
@@ -272,7 +306,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
case "tool-result": {
|
case "tool-result": {
|
||||||
const tool = tools.get(event.id)
|
const tool = tools.get(event.id)
|
||||||
if (!tool?.called) return yield* Effect.die(`Tool result before call: ${event.id}`)
|
if (!tool?.called) return yield* Effect.die(`Tool result before call: ${event.id}`)
|
||||||
if (tool.name !== event.name) return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
if (tool.name !== event.name)
|
||||||
|
return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||||
if (tool.settled) {
|
if (tool.settled) {
|
||||||
if (event.result.type === "error") return
|
if (event.result.type === "error") return
|
||||||
return yield* Effect.die(`Duplicate tool result: ${event.id}`)
|
return yield* Effect.die(`Duplicate tool result: ${event.id}`)
|
||||||
@@ -309,7 +344,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||||||
case "tool-error": {
|
case "tool-error": {
|
||||||
const tool = tools.get(event.id)
|
const tool = tools.get(event.id)
|
||||||
if (!tool?.called) return yield* Effect.die(`Tool error before call: ${event.id}`)
|
if (!tool?.called) return yield* Effect.die(`Tool error before call: ${event.id}`)
|
||||||
if (tool.name !== event.name) return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
if (tool.name !== event.name)
|
||||||
|
return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`)
|
||||||
if (tool.settled) return yield* Effect.die(`Duplicate tool error: ${event.id}`)
|
if (tool.settled) return yield* Effect.die(`Duplicate tool error: ${event.id}`)
|
||||||
tool.settled = true
|
tool.settled = true
|
||||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Message, ToolCallPart, ToolOutput, ToolResultPart, type ContentPart, type Model, type ProviderMetadata } from "@opencode-ai/llm"
|
import {
|
||||||
|
Message,
|
||||||
|
ToolCallPart,
|
||||||
|
ToolOutput,
|
||||||
|
ToolResultPart,
|
||||||
|
type ContentPart,
|
||||||
|
type Model,
|
||||||
|
type ProviderMetadata,
|
||||||
|
} from "@opencode-ai/llm"
|
||||||
import { SessionMessage } from "../message"
|
import { SessionMessage } from "../message"
|
||||||
import type { FileAttachment } from "../prompt"
|
import type { FileAttachment } from "../prompt"
|
||||||
|
|
||||||
@@ -61,7 +69,8 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
||||||
const sameModel = String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
|
const sameModel =
|
||||||
|
String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
|
||||||
const content = message.content.flatMap((item): ContentPart[] => {
|
const content = message.content.flatMap((item): ContentPart[] => {
|
||||||
if (item.type === "text") return [{ type: "text", text: item.text }]
|
if (item.type === "text") return [{ type: "text", text: item.text }]
|
||||||
if (item.type === "reasoning")
|
if (item.type === "reasoning")
|
||||||
@@ -71,12 +80,12 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
|||||||
? [{ type: "text", text: item.text }]
|
? [{ type: "text", text: item.text }]
|
||||||
: []
|
: []
|
||||||
const call = toolCall(item, sameModel ? item.provider?.metadata : undefined)
|
const call = toolCall(item, sameModel ? item.provider?.metadata : undefined)
|
||||||
const result = toolResult(item, sameModel ? item.provider?.resultMetadata ?? item.provider?.metadata : undefined)
|
const result = toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined)
|
||||||
return item.provider?.executed === true && result ? [call, result] : [call]
|
return item.provider?.executed === true && result ? [call, result] : [call]
|
||||||
})
|
})
|
||||||
const results = message.content
|
const results = message.content
|
||||||
.filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.provider?.executed !== true)
|
.filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.provider?.executed !== true)
|
||||||
.map((item) => toolResult(item, sameModel ? item.provider?.resultMetadata ?? item.provider?.metadata : undefined))
|
.map((item) => toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined))
|
||||||
.filter((message) => message !== undefined)
|
.filter((message) => message !== undefined)
|
||||||
.map(Message.tool)
|
.map(Message.tool)
|
||||||
return [Message.make({ id: message.id, role: "assistant", content, metadata: message.metadata }), ...results]
|
return [Message.make({ id: message.id, role: "assistant", content, metadata: message.metadata }), ...results]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
|
|||||||
const create = () => schema.make("ses_" + Identifier.descending())
|
const create = () => schema.make("ses_" + Identifier.descending())
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
descending: (id?: string) => id === undefined ? create() : schema.make(id),
|
descending: (id?: string) => (id === undefined ? create() : schema.make(id)),
|
||||||
fromExternal: (input: ExternalID) => schema.make(externalID("ses", input)),
|
fromExternal: (input: ExternalID) => schema.make(externalID("ses", input)),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -151,11 +151,10 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
({ skill, root, files }) =>
|
({ skill, root, files }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(files, (file) => download(file.url, file.destination), {
|
||||||
files,
|
concurrency: fileConcurrency,
|
||||||
(file) => download(file.url, file.destination),
|
discard: true,
|
||||||
{ concurrency: fileConcurrency, discard: true },
|
})
|
||||||
)
|
|
||||||
return (yield* fs.exists(path.join(root, "SKILL.md")).pipe(Effect.orDie)) ||
|
return (yield* fs.exists(path.join(root, "SKILL.md")).pipe(Effect.orDie)) ||
|
||||||
(yield* fs.exists(path.join(root, `${skill.name}.md`)).pipe(Effect.orDie))
|
(yield* fs.exists(path.join(root, `${skill.name}.md`)).pipe(Effect.orDie))
|
||||||
? [AbsolutePath.make(root)]
|
? [AbsolutePath.make(root)]
|
||||||
|
|||||||
@@ -33,14 +33,20 @@ export class Page extends Schema.Class<Page>("ToolOutputStore.Page")({
|
|||||||
next: NonNegativeInt.pipe(Schema.optional),
|
next: NonNegativeInt.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export class AccessDeniedError extends Schema.TaggedErrorClass<AccessDeniedError>()("ToolOutputStore.AccessDeniedError", {
|
export class AccessDeniedError extends Schema.TaggedErrorClass<AccessDeniedError>()(
|
||||||
|
"ToolOutputStore.AccessDeniedError",
|
||||||
|
{
|
||||||
uri: Schema.String,
|
uri: Schema.String,
|
||||||
sessionID: SessionSchema.ID,
|
sessionID: SessionSchema.ID,
|
||||||
}) {}
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
export class InvalidResourceError extends Schema.TaggedErrorClass<InvalidResourceError>()("ToolOutputStore.InvalidResourceError", {
|
export class InvalidResourceError extends Schema.TaggedErrorClass<InvalidResourceError>()(
|
||||||
|
"ToolOutputStore.InvalidResourceError",
|
||||||
|
{
|
||||||
uri: Schema.String,
|
uri: Schema.String,
|
||||||
}) {}
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
export class ResourceNotFoundError extends Schema.TaggedErrorClass<ResourceNotFoundError>()(
|
export class ResourceNotFoundError extends Schema.TaggedErrorClass<ResourceNotFoundError>()(
|
||||||
"ToolOutputStore.ResourceNotFoundError",
|
"ToolOutputStore.ResourceNotFoundError",
|
||||||
@@ -88,7 +94,9 @@ export interface Interface {
|
|||||||
readonly limits: () => Effect.Effect<{ readonly maxLines: number; readonly maxBytes: number }>
|
readonly limits: () => Effect.Effect<{ readonly maxLines: number; readonly maxBytes: number }>
|
||||||
readonly write: (input: WriteInput) => Effect.Effect<Resource>
|
readonly write: (input: WriteInput) => Effect.Effect<Resource>
|
||||||
readonly truncate: (input: TruncateInput) => Effect.Effect<TruncateResult>
|
readonly truncate: (input: TruncateInput) => Effect.Effect<TruncateResult>
|
||||||
readonly read: (input: ReadInput) => Effect.Effect<Page, AccessDeniedError | InvalidResourceError | ResourceNotFoundError>
|
readonly read: (
|
||||||
|
input: ReadInput,
|
||||||
|
) => Effect.Effect<Page, AccessDeniedError | InvalidResourceError | ResourceNotFoundError>
|
||||||
readonly cleanup: () => Effect.Effect<void>
|
readonly cleanup: () => Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,11 +161,17 @@ const preview = (text: string, maxLines: number, maxBytes: number) => {
|
|||||||
const sampled =
|
const sampled =
|
||||||
lines.length <= maxLines
|
lines.length <= maxLines
|
||||||
? text
|
? text
|
||||||
: [lines.slice(0, headLines).join("\n"), ...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : [])].join("\n")
|
: [
|
||||||
|
lines.slice(0, headLines).join("\n"),
|
||||||
|
...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : []),
|
||||||
|
].join("\n")
|
||||||
if (Buffer.byteLength(sampled, "utf-8") <= maxBytes) {
|
if (Buffer.byteLength(sampled, "utf-8") <= maxBytes) {
|
||||||
return lines.length <= maxLines
|
return lines.length <= maxLines
|
||||||
? { head: sampled, tail: "" }
|
? { head: sampled, tail: "" }
|
||||||
: { head: lines.slice(0, headLines).join("\n"), tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "" }
|
: {
|
||||||
|
head: lines.slice(0, headLines).join("\n"),
|
||||||
|
tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const headBytes = Math.ceil(maxBytes / 2)
|
const headBytes = Math.ceil(maxBytes / 2)
|
||||||
const tailBytes = Math.floor(maxBytes / 2)
|
const tailBytes = Math.floor(maxBytes / 2)
|
||||||
@@ -192,7 +206,7 @@ export const layer = Layer.effect(
|
|||||||
const entries = yield* config.value.entries().pipe(Effect.catch(() => Effect.succeed([] as Config.Entry[])))
|
const entries = yield* config.value.entries().pipe(Effect.catch(() => Effect.succeed([] as Config.Entry[])))
|
||||||
const configured = Object.assign(
|
const configured = Object.assign(
|
||||||
{},
|
{},
|
||||||
...entries.flatMap((entry) => entry.type === "document" ? [entry.info.tool_output ?? {}] : []),
|
...entries.flatMap((entry) => (entry.type === "document" ? [entry.info.tool_output ?? {}] : [])),
|
||||||
)
|
)
|
||||||
return { maxLines: configured.max_lines ?? MAX_LINES, maxBytes: configured.max_bytes ?? MAX_BYTES }
|
return { maxLines: configured.max_lines ?? MAX_LINES, maxBytes: configured.max_bytes ?? MAX_BYTES }
|
||||||
})
|
})
|
||||||
@@ -218,7 +232,12 @@ export const layer = Layer.effect(
|
|||||||
Effect.onError(() => fs.remove(contentPath(id)).pipe(Effect.catch(() => Effect.void))),
|
Effect.onError(() => fs.remove(contentPath(id)).pipe(Effect.catch(() => Effect.void))),
|
||||||
Effect.orDie,
|
Effect.orDie,
|
||||||
)
|
)
|
||||||
return new Resource({ uri: resourceUri, mime: record.mime, ...(record.name === undefined ? {} : { name: record.name }), size })
|
return new Resource({
|
||||||
|
uri: resourceUri,
|
||||||
|
mime: record.mime,
|
||||||
|
...(record.name === undefined ? {} : { name: record.name }),
|
||||||
|
size,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const truncate = Effect.fn("ToolOutputStore.truncate")(function* (input: TruncateInput) {
|
const truncate = Effect.fn("ToolOutputStore.truncate")(function* (input: TruncateInput) {
|
||||||
@@ -281,12 +300,14 @@ export const layer = Layer.effect(
|
|||||||
const cleanup = Effect.fn("ToolOutputStore.cleanup")(function* () {
|
const cleanup = Effect.fn("ToolOutputStore.cleanup")(function* () {
|
||||||
const entries = yield* fs.readDirectory(directory).pipe(Effect.catch(() => Effect.succeed([])))
|
const entries = yield* fs.readDirectory(directory).pipe(Effect.catch(() => Effect.succeed([])))
|
||||||
const cutoff = Date.now() - Duration.toMillis(RETENTION)
|
const cutoff = Date.now() - Duration.toMillis(RETENTION)
|
||||||
const ids = new Set(entries.flatMap((entry) => {
|
const ids = new Set(
|
||||||
|
entries.flatMap((entry) => {
|
||||||
const match = entry.match(/^([0-9a-f]{12}[0-9A-Za-z]{14})\.(?:json|txt)$/)
|
const match = entry.match(/^([0-9a-f]{12}[0-9A-Za-z]{14})\.(?:json|txt)$/)
|
||||||
return match ? [match[1]] : []
|
return match ? [match[1]] : []
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
const removeIfPresent = (target: string) =>
|
const removeIfPresent = (target: string) =>
|
||||||
fs.existsSafe(target).pipe(Effect.flatMap((exists) => exists ? fs.remove(target) : Effect.void))
|
fs.existsSafe(target).pipe(Effect.flatMap((exists) => (exists ? fs.remove(target) : Effect.void)))
|
||||||
const removePair = (id: string) =>
|
const removePair = (id: string) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* removeIfPresent(contentPath(id))
|
yield* removeIfPresent(contentPath(id))
|
||||||
@@ -298,13 +319,19 @@ export const layer = Layer.effect(
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
if (!contentExists) continue
|
if (!contentExists) continue
|
||||||
const info = yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void))
|
const info = yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void))
|
||||||
const modified = info ? info.mtime.pipe(Option.map((date) => date.getTime()), Option.getOrElse(() => 0)) : 0
|
const modified = info
|
||||||
|
? info.mtime.pipe(
|
||||||
|
Option.map((date) => date.getTime()),
|
||||||
|
Option.getOrElse(() => 0),
|
||||||
|
)
|
||||||
|
: 0
|
||||||
if (modified < cutoff) yield* removePair(id)
|
if (modified < cutoff) yield* removePair(id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const record = yield* Effect.try({ try: () => JSON.parse(text), catch: () => new globalThis.Error("Invalid metadata") }).pipe(
|
const record = yield* Effect.try({
|
||||||
Effect.catch(() => Effect.succeed(undefined)),
|
try: () => JSON.parse(text),
|
||||||
)
|
catch: () => new globalThis.Error("Invalid metadata"),
|
||||||
|
}).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||||
const info = contentExists ? yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void)) : undefined
|
const info = contentExists ? yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void)) : undefined
|
||||||
if (
|
if (
|
||||||
!contentExists ||
|
!contentExists ||
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
export * as ToolRegistry from "./tool-registry"
|
export * as ToolRegistry from "./tool-registry"
|
||||||
|
|
||||||
import { Tool, ToolFailure, ToolOutput, ToolResultValue as ToolResult, type Tool as TypedTool, type ToolCall, type ToolResultValue, type ToolSchema, type ToolSettlement } from "@opencode-ai/llm"
|
import {
|
||||||
|
Tool,
|
||||||
|
ToolFailure,
|
||||||
|
ToolOutput,
|
||||||
|
ToolResultValue as ToolResult,
|
||||||
|
type Tool as TypedTool,
|
||||||
|
type ToolCall,
|
||||||
|
type ToolResultValue,
|
||||||
|
type ToolSchema,
|
||||||
|
type ToolSettlement,
|
||||||
|
} from "@opencode-ai/llm"
|
||||||
import { Context, Effect, Layer, Schema, Scope } from "effect"
|
import { Context, Effect, Layer, Schema, Scope } from "effect"
|
||||||
import { castDraft, enableMapSet } from "immer"
|
import { castDraft, enableMapSet } from "immer"
|
||||||
import { PermissionV2 } from "./permission"
|
import { PermissionV2 } from "./permission"
|
||||||
@@ -35,10 +45,15 @@ export type AuthorizeInput<Parameters = unknown> = Invocation & {
|
|||||||
readonly parameters: Parameters
|
readonly parameters: Parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Entry<Parameters extends ToolSchema<any> = ToolSchema<any>, Success extends ToolSchema<any> = ToolSchema<any>> = {
|
export type Entry<
|
||||||
|
Parameters extends ToolSchema<any> = ToolSchema<any>,
|
||||||
|
Success extends ToolSchema<any> = ToolSchema<any>,
|
||||||
|
> = {
|
||||||
readonly tool: TypedTool<Parameters, Success>
|
readonly tool: TypedTool<Parameters, Success>
|
||||||
readonly authorize?: (input: AuthorizeInput<Schema.Schema.Type<Parameters>>) => Effect.Effect<void, ToolFailure>
|
readonly authorize?: (input: AuthorizeInput<Schema.Schema.Type<Parameters>>) => Effect.Effect<void, ToolFailure>
|
||||||
readonly execute?: (input: AuthorizeInput<Schema.Schema.Type<Parameters>>) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
|
readonly execute?: (
|
||||||
|
input: AuthorizeInput<Schema.Schema.Type<Parameters>>,
|
||||||
|
) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
@@ -48,7 +63,10 @@ type Data = {
|
|||||||
export type Editor = {
|
export type Editor = {
|
||||||
readonly list: () => ReadonlyArray<readonly [string, Entry]>
|
readonly list: () => ReadonlyArray<readonly [string, Entry]>
|
||||||
readonly get: (name: string) => Entry | undefined
|
readonly get: (name: string) => Entry | undefined
|
||||||
readonly set: <Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(name: string, entry: Entry<Parameters, Success>) => void
|
readonly set: <Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(
|
||||||
|
name: string,
|
||||||
|
entry: Entry<Parameters, Success>,
|
||||||
|
) => void
|
||||||
readonly remove: (name: string) => void
|
readonly remove: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +92,10 @@ export const layer = Layer.effect(
|
|||||||
list: () => Array.from(draft.entries.entries()) as Array<[string, Entry]>,
|
list: () => Array.from(draft.entries.entries()) as Array<[string, Entry]>,
|
||||||
get: (name) => draft.entries.get(name) as Entry | undefined,
|
get: (name) => draft.entries.get(name) as Entry | undefined,
|
||||||
set: (name, entry) => {
|
set: (name, entry) => {
|
||||||
draft.entries.set(name, castDraft(entry) as typeof draft.entries extends Map<string, infer Value> ? Value : never)
|
draft.entries.set(
|
||||||
|
name,
|
||||||
|
castDraft(entry) as typeof draft.entries extends Map<string, infer Value> ? Value : never,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
remove: (name) => {
|
remove: (name) => {
|
||||||
draft.entries.delete(name)
|
draft.entries.delete(name)
|
||||||
@@ -83,7 +104,9 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const definitions = Effect.fn("ToolRegistry.definitions")(function* () {
|
const definitions = Effect.fn("ToolRegistry.definitions")(function* () {
|
||||||
return Tool.toDefinitions(Object.fromEntries(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool])))
|
return Tool.toDefinitions(
|
||||||
|
Object.fromEntries(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool])),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const invocation = (input: ExecuteInput): Invocation => ({
|
const invocation = (input: ExecuteInput): Invocation => ({
|
||||||
@@ -102,9 +125,11 @@ export const layer = Layer.effect(
|
|||||||
Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })),
|
Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })),
|
||||||
Effect.flatMap((parameters) => {
|
Effect.flatMap((parameters) => {
|
||||||
const context = { ...invocation(input), parameters }
|
const context = { ...invocation(input), parameters }
|
||||||
const execute = entry.execute?.(context) ??
|
const execute =
|
||||||
entry.tool.execute!(parameters, { id: input.call.id, name: input.call.name })
|
entry.execute?.(context) ?? entry.tool.execute!(parameters, { id: input.call.id, name: input.call.name })
|
||||||
return (entry.authorize === undefined ? execute : entry.authorize(context).pipe(Effect.andThen(execute))).pipe(
|
return (
|
||||||
|
entry.authorize === undefined ? execute : entry.authorize(context).pipe(Effect.andThen(execute))
|
||||||
|
).pipe(
|
||||||
Effect.flatMap((value) =>
|
Effect.flatMap((value) =>
|
||||||
entry.tool._encode(value).pipe(
|
entry.tool._encode(value).pipe(
|
||||||
Effect.mapError(
|
Effect.mapError(
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { ToolRegistry } from "../tool-registry"
|
|||||||
export const name = "apply_patch"
|
export const name = "apply_patch"
|
||||||
|
|
||||||
export const Parameters = Schema.Struct({
|
export const Parameters = Schema.Struct({
|
||||||
patchText: Schema.String.annotate({ description: "The full patch text describing add, update, and delete operations" }),
|
patchText: Schema.String.annotate({
|
||||||
|
description: "The full patch text describing add, update, and delete operations",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Applied = Schema.Struct({
|
export const Applied = Schema.Struct({
|
||||||
@@ -24,7 +26,12 @@ export const Success = Schema.Struct({ applied: Schema.Array(Applied) })
|
|||||||
export type Success = typeof Success.Type
|
export type Success = typeof Success.Type
|
||||||
|
|
||||||
export const toModelOutput = (output: Success) =>
|
export const toModelOutput = (output: Success) =>
|
||||||
["Applied patch sequentially:", ...output.applied.map((item) => `${item.type === "add" ? "A" : item.type === "delete" ? "D" : "M"} ${item.resource}`)].join("\n")
|
[
|
||||||
|
"Applied patch sequentially:",
|
||||||
|
...output.applied.map(
|
||||||
|
(item) => `${item.type === "add" ? "A" : item.type === "delete" ? "D" : "M"} ${item.resource}`,
|
||||||
|
),
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
const definition = Tool.make({
|
const definition = Tool.make({
|
||||||
description:
|
description:
|
||||||
@@ -36,9 +43,23 @@ const definition = Tool.make({
|
|||||||
|
|
||||||
type Planned = { readonly hunk: Patch.Hunk; readonly plan: LocationMutation.Plan }
|
type Planned = { readonly hunk: Patch.Hunk; readonly plan: LocationMutation.Plan }
|
||||||
type Prepared =
|
type Prepared =
|
||||||
| { readonly type: "add"; readonly hunk: Extract<Patch.Hunk, { readonly type: "add" }>; readonly plan: LocationMutation.Plan }
|
| {
|
||||||
| { readonly type: "delete"; readonly hunk: Extract<Patch.Hunk, { readonly type: "delete" }>; readonly plan: LocationMutation.Plan }
|
readonly type: "add"
|
||||||
| { readonly type: "update"; readonly hunk: Extract<Patch.Hunk, { readonly type: "update" }>; readonly plan: LocationMutation.Plan; readonly source: Uint8Array; readonly content: string }
|
readonly hunk: Extract<Patch.Hunk, { readonly type: "add" }>
|
||||||
|
readonly plan: LocationMutation.Plan
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly type: "delete"
|
||||||
|
readonly hunk: Extract<Patch.Hunk, { readonly type: "delete" }>
|
||||||
|
readonly plan: LocationMutation.Plan
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly type: "update"
|
||||||
|
readonly hunk: Extract<Patch.Hunk, { readonly type: "update" }>
|
||||||
|
readonly plan: LocationMutation.Plan
|
||||||
|
readonly source: Uint8Array
|
||||||
|
readonly content: string
|
||||||
|
}
|
||||||
|
|
||||||
export const layer = Layer.effectDiscard(
|
export const layer = Layer.effectDiscard(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -53,7 +74,8 @@ export const layer = Layer.effectDiscard(
|
|||||||
execute: ({ parameters, assertPermission }) => {
|
execute: ({ parameters, assertPermission }) => {
|
||||||
const applied: Array<typeof Applied.Type> = []
|
const applied: Array<typeof Applied.Type> = []
|
||||||
const fail = (path: string, cause: unknown) => {
|
const fail = (path: string, cause: unknown) => {
|
||||||
const prefix = applied.length === 0
|
const prefix =
|
||||||
|
applied.length === 0
|
||||||
? `Unable to apply patch at ${path}`
|
? `Unable to apply patch at ${path}`
|
||||||
: `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}`
|
: `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}`
|
||||||
return new ToolFailure({ message: prefix, error: cause })
|
return new ToolFailure({ message: prefix, error: cause })
|
||||||
@@ -69,7 +91,8 @@ export const layer = Layer.effectDiscard(
|
|||||||
if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" })
|
if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" })
|
||||||
|
|
||||||
const planned: Planned[] = []
|
const planned: Planned[] = []
|
||||||
for (const hunk of hunks) planned.push({ hunk, plan: yield* mutation.resolve({ path: hunk.path, kind: "file" }) })
|
for (const hunk of hunks)
|
||||||
|
planned.push({ hunk, plan: yield* mutation.resolve({ path: hunk.path, kind: "file" }) })
|
||||||
const externalDirectories = new Map<string, LocationMutation.ExternalDirectoryAuthorization>()
|
const externalDirectories = new Map<string, LocationMutation.ExternalDirectoryAuthorization>()
|
||||||
for (const { plan } of planned) {
|
for (const { plan } of planned) {
|
||||||
const external = plan.target.externalDirectory
|
const external = plan.target.externalDirectory
|
||||||
@@ -78,7 +101,11 @@ export const layer = Layer.effectDiscard(
|
|||||||
for (const external of externalDirectories.values()) {
|
for (const external of externalDirectories.values()) {
|
||||||
yield* assertPermission(LocationMutation.externalDirectoryPermission(external))
|
yield* assertPermission(LocationMutation.externalDirectoryPermission(external))
|
||||||
}
|
}
|
||||||
yield* assertPermission({ action: "edit", resources: [...new Set(planned.map(({ plan }) => plan.target.resource))], save: ["*"] })
|
yield* assertPermission({
|
||||||
|
action: "edit",
|
||||||
|
resources: [...new Set(planned.map(({ plan }) => plan.target.resource))],
|
||||||
|
save: ["*"],
|
||||||
|
})
|
||||||
|
|
||||||
const prepared: Prepared[] = []
|
const prepared: Prepared[] = []
|
||||||
for (const { hunk, plan } of planned) {
|
for (const { hunk, plan } of planned) {
|
||||||
@@ -89,21 +116,34 @@ export const layer = Layer.effectDiscard(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const target = yield* mutation.revalidate(plan)
|
const target = yield* mutation.revalidate(plan)
|
||||||
if (!target.exists || target.type !== "File") return yield* fail(hunk.path, new Error("Target file does not exist"))
|
if (!target.exists || target.type !== "File")
|
||||||
|
return yield* fail(hunk.path, new Error("Target file does not exist"))
|
||||||
if (hunk.type === "delete") {
|
if (hunk.type === "delete") {
|
||||||
prepared.push({ type: hunk.type, hunk, plan })
|
prepared.push({ type: hunk.type, hunk, plan })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const source = yield* fs.readFile(target.canonical)
|
const source = yield* fs.readFile(target.canonical)
|
||||||
const update = Patch.derive(hunk.path, hunk.chunks, new TextDecoder("utf-8", { ignoreBOM: true }).decode(source))
|
const update = Patch.derive(
|
||||||
|
hunk.path,
|
||||||
|
hunk.chunks,
|
||||||
|
new TextDecoder("utf-8", { ignoreBOM: true }).decode(source),
|
||||||
|
)
|
||||||
prepared.push({ type: hunk.type, hunk, plan, source, content: Patch.joinBom(update.content, update.bom) })
|
prepared.push({ type: hunk.type, hunk, plan, source, content: Patch.joinBom(update.content, update.bom) })
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.uninterruptible(
|
yield* Effect.uninterruptible(
|
||||||
Effect.forEach(prepared, (change) =>
|
Effect.forEach(
|
||||||
|
prepared,
|
||||||
|
(change) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (change.type === "add") {
|
if (change.type === "add") {
|
||||||
const result = yield* files.create({ plan: change.plan, content: change.hunk.contents.endsWith("\n") || change.hunk.contents === "" ? change.hunk.contents : `${change.hunk.contents}\n` })
|
const result = yield* files.create({
|
||||||
|
plan: change.plan,
|
||||||
|
content:
|
||||||
|
change.hunk.contents.endsWith("\n") || change.hunk.contents === ""
|
||||||
|
? change.hunk.contents
|
||||||
|
: `${change.hunk.contents}\n`,
|
||||||
|
})
|
||||||
applied.push({ type: change.type, resource: result.resource, target: result.target })
|
applied.push({ type: change.type, resource: result.resource, target: result.target })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -112,10 +152,15 @@ export const layer = Layer.effectDiscard(
|
|||||||
applied.push({ type: change.type, resource: result.resource, target: result.target })
|
applied.push({ type: change.type, resource: result.resource, target: result.target })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = yield* files.writeIfUnchanged({ plan: change.plan, expected: change.source, content: change.content })
|
const result = yield* files.writeIfUnchanged({
|
||||||
|
plan: change.plan,
|
||||||
|
expected: change.source,
|
||||||
|
content: change.content,
|
||||||
|
})
|
||||||
applied.push({ type: change.type, resource: result.resource, target: result.target })
|
applied.push({ type: change.type, resource: result.resource, target: result.target })
|
||||||
}).pipe(Effect.catchCause((cause) => Effect.fail(fail(change.hunk.path, Cause.squash(cause))))),
|
}).pipe(Effect.catchCause((cause) => Effect.fail(fail(change.hunk.path, Cause.squash(cause))))),
|
||||||
{ discard: true }),
|
{ discard: true },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return { applied }
|
return { applied }
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ export const layer = Layer.effectDiscard(
|
|||||||
const error = Cause.squash(cause)
|
const error = Cause.squash(cause)
|
||||||
return Effect.fail(
|
return Effect.fail(
|
||||||
error instanceof FileMutation.StaleContentError
|
error instanceof FileMutation.StaleContentError
|
||||||
? new ToolFailure({ message: "File changed after permission approval. Read it again before editing." })
|
? new ToolFailure({
|
||||||
|
message: "File changed after permission approval. Read it again before editing.",
|
||||||
|
})
|
||||||
: new ToolFailure({ message: `Unable to edit ${parameters.path}`, error }),
|
: new ToolFailure({ message: `Unable to edit ${parameters.path}`, error }),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -123,7 +125,9 @@ export const layer = Layer.effectDiscard(
|
|||||||
return yield* new ToolFailure({ message: "No changes to apply: oldString and newString are identical." })
|
return yield* new ToolFailure({ message: "No changes to apply: oldString and newString are identical." })
|
||||||
}
|
}
|
||||||
if (parameters.oldString === "") {
|
if (parameters.oldString === "") {
|
||||||
return yield* new ToolFailure({ message: "oldString must not be empty. Use write to create or overwrite a file." })
|
return yield* new ToolFailure({
|
||||||
|
message: "oldString must not be empty. Use write to create or overwrite a file.",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = yield* unableToEdit(mutation.resolve({ path: parameters.path, kind: "file" }))
|
const plan = yield* unableToEdit(mutation.resolve({ path: parameters.path, kind: "file" }))
|
||||||
@@ -158,7 +162,11 @@ export const layer = Layer.effectDiscard(
|
|||||||
: source.text.replace(oldString, newString)
|
: source.text.replace(oldString, newString)
|
||||||
const next = splitBom(replaced)
|
const next = splitBom(replaced)
|
||||||
const result = yield* unableToEdit(
|
const result = yield* unableToEdit(
|
||||||
files.writeIfUnchanged({ plan, expected: source.content, content: joinBom(next.text, source.bom || next.bom) }),
|
files.writeIfUnchanged({
|
||||||
|
plan,
|
||||||
|
expected: source.content,
|
||||||
|
content: joinBom(next.text, source.bom || next.bom),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
return { ...result, replacements } satisfies Success
|
return { ...result, replacements } satisfies Success
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ export const name = "glob"
|
|||||||
|
|
||||||
export const Parameters = Schema.Struct({
|
export const Parameters = Schema.Struct({
|
||||||
pattern: LocationSearch.FilesInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }),
|
pattern: LocationSearch.FilesInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }),
|
||||||
path: LocationSearch.FilesInput.fields.path.annotate({ description: "Relative directory to search. Defaults to the active Location." }),
|
path: LocationSearch.FilesInput.fields.path.annotate({
|
||||||
reference: LocationSearch.FilesInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }),
|
description: "Relative directory to search. Defaults to the active Location.",
|
||||||
limit: LocationSearch.FilesInput.fields.limit.annotate({ description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }),
|
}),
|
||||||
|
reference: LocationSearch.FilesInput.fields.reference.annotate({
|
||||||
|
description: "Named project reference to search instead of the active Location",
|
||||||
|
}),
|
||||||
|
limit: LocationSearch.FilesInput.fields.limit.annotate({
|
||||||
|
description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
type ModelOutput = typeof LocationSearch.FilesResult.Encoded
|
type ModelOutput = typeof LocationSearch.FilesResult.Encoded
|
||||||
@@ -21,14 +27,18 @@ type ModelOutput = typeof LocationSearch.FilesResult.Encoded
|
|||||||
export const toModelOutput = (output: ModelOutput) => {
|
export const toModelOutput = (output: ModelOutput) => {
|
||||||
const lines = output.items.length === 0 ? ["No files found"] : output.items.map((item) => item.resource)
|
const lines = output.items.length === 0 ? ["No files found"] : output.items.map((item) => item.resource)
|
||||||
if (output.truncated) {
|
if (output.truncated) {
|
||||||
lines.push("", `(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`)
|
lines.push(
|
||||||
|
"",
|
||||||
|
`(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (output.partial) lines.push("", "(Results may be incomplete because some discovered files could not be read.)")
|
if (output.partial) lines.push("", "(Results may be incomplete because some discovered files could not be read.)")
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = Tool.make({
|
const definition = Tool.make({
|
||||||
description: "Find files by glob pattern within the active Location or a named project reference. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
|
description:
|
||||||
|
"Find files by glob pattern within the active Location or a named project reference. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
|
||||||
parameters: Parameters,
|
parameters: Parameters,
|
||||||
success: LocationSearch.FilesResult,
|
success: LocationSearch.FilesResult,
|
||||||
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
|
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
|
||||||
@@ -66,7 +76,12 @@ export const layer = Layer.effectDiscard(
|
|||||||
return yield* search.files(parameters, root)
|
return yield* search.files(parameters, root)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchCause((cause) =>
|
Effect.catchCause((cause) =>
|
||||||
Effect.fail(new ToolFailure({ message: `Unable to find files matching ${parameters.pattern}`, error: Cause.squash(cause) })),
|
Effect.fail(
|
||||||
|
new ToolFailure({
|
||||||
|
message: `Unable to find files matching ${parameters.pattern}`,
|
||||||
|
error: Cause.squash(cause),
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,11 +10,21 @@ import { ToolRegistry } from "../tool-registry"
|
|||||||
export const name = "grep"
|
export const name = "grep"
|
||||||
|
|
||||||
export const Parameters = Schema.Struct({
|
export const Parameters = Schema.Struct({
|
||||||
pattern: LocationSearch.GrepInput.fields.pattern.annotate({ description: "Regex pattern to search for in file contents" }),
|
pattern: LocationSearch.GrepInput.fields.pattern.annotate({
|
||||||
path: LocationSearch.GrepInput.fields.path.annotate({ description: "Relative file or directory to search. Defaults to the active Location." }),
|
description: "Regex pattern to search for in file contents",
|
||||||
reference: LocationSearch.GrepInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }),
|
}),
|
||||||
include: LocationSearch.GrepInput.fields.include.annotate({ description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")' }),
|
path: LocationSearch.GrepInput.fields.path.annotate({
|
||||||
limit: LocationSearch.GrepInput.fields.limit.annotate({ description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }),
|
description: "Relative file or directory to search. Defaults to the active Location.",
|
||||||
|
}),
|
||||||
|
reference: LocationSearch.GrepInput.fields.reference.annotate({
|
||||||
|
description: "Named project reference to search instead of the active Location",
|
||||||
|
}),
|
||||||
|
include: LocationSearch.GrepInput.fields.include.annotate({
|
||||||
|
description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")',
|
||||||
|
}),
|
||||||
|
limit: LocationSearch.GrepInput.fields.limit.annotate({
|
||||||
|
description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Success = typeof LocationSearch.GrepResult.Encoded
|
type Success = typeof LocationSearch.GrepResult.Encoded
|
||||||
@@ -32,14 +42,18 @@ export const toModelOutput = (output: Success) => {
|
|||||||
lines.push(` Line ${match.line}: ${match.lines}${match.linePreviewTruncated ? "..." : ""}`)
|
lines.push(` Line ${match.line}: ${match.lines}${match.linePreviewTruncated ? "..." : ""}`)
|
||||||
}
|
}
|
||||||
if (output.truncated) {
|
if (output.truncated) {
|
||||||
lines.push("", `(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`)
|
lines.push(
|
||||||
|
"",
|
||||||
|
`(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (output.partial) lines.push("", "(Some paths were inaccessible and skipped)")
|
if (output.partial) lines.push("", "(Some paths were inaccessible and skipped)")
|
||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = Tool.make({
|
const definition = Tool.make({
|
||||||
description: "Search file contents by regular expression within the active Location or a named project reference. Use a relative path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise relative file resources, line numbers, and bounded line previews.",
|
description:
|
||||||
|
"Search file contents by regular expression within the active Location or a named project reference. Use a relative path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise relative file resources, line numbers, and bounded line previews.",
|
||||||
parameters: Parameters,
|
parameters: Parameters,
|
||||||
success: LocationSearch.GrepResult,
|
success: LocationSearch.GrepResult,
|
||||||
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
|
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
|
||||||
@@ -79,7 +93,8 @@ export const layer = Layer.effectDiscard(
|
|||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchCause((cause) => {
|
Effect.catchCause((cause) => {
|
||||||
const error = Cause.squash(cause)
|
const error = Cause.squash(cause)
|
||||||
const message = error instanceof Ripgrep.InvalidPatternError
|
const message =
|
||||||
|
error instanceof Ripgrep.InvalidPatternError
|
||||||
? `Invalid grep pattern ${JSON.stringify(parameters.pattern)}: ${error.message}`
|
? `Invalid grep pattern ${JSON.stringify(parameters.pattern)}: ${error.message}`
|
||||||
: `Unable to grep for ${parameters.pattern}`
|
: `Unable to grep for ${parameters.pattern}`
|
||||||
return Effect.fail(new ToolFailure({ message, error }))
|
return Effect.fail(new ToolFailure({ message, error }))
|
||||||
|
|||||||
@@ -70,7 +70,11 @@ export const layer = Layer.effectDiscard(
|
|||||||
const final = yield* filesystem.resolveReadPath(input)
|
const final = yield* filesystem.resolveReadPath(input)
|
||||||
if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real)
|
if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real)
|
||||||
return yield* Effect.die(new Error("File changed after permission approval"))
|
return yield* Effect.die(new Error("File changed after permission approval"))
|
||||||
if (final.target.size > FileSystem.MAX_READ_BYTES || input.offset !== undefined || input.limit !== undefined)
|
if (
|
||||||
|
final.target.size > FileSystem.MAX_READ_BYTES ||
|
||||||
|
input.offset !== undefined ||
|
||||||
|
input.limit !== undefined
|
||||||
|
)
|
||||||
return yield* filesystem.readTextPageResolved(final.target, { offset: input.offset, limit: input.limit })
|
return yield* filesystem.readTextPageResolved(final.target, { offset: input.offset, limit: input.limit })
|
||||||
return yield* filesystem.readResolved(final.target, FileSystem.MAX_READ_BYTES)
|
return yield* filesystem.readResolved(final.target, FileSystem.MAX_READ_BYTES)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
|||||||
@@ -150,7 +150,9 @@ export const layer = Layer.effectDiscard(
|
|||||||
|
|
||||||
const { body, contentType } = yield* Effect.gen(function* () {
|
const { body, contentType } = yield* Effect.gen(function* () {
|
||||||
const response = yield* execute(http, parameters.url, parameters.format).pipe(
|
const response = yield* execute(http, parameters.url, parameters.format).pipe(
|
||||||
Effect.catchIf(isCloudflareChallenge, () => execute(http, parameters.url, parameters.format, "opencode")),
|
Effect.catchIf(isCloudflareChallenge, () =>
|
||||||
|
execute(http, parameters.url, parameters.format, "opencode"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const contentType = response.headers["content-type"] || ""
|
const contentType = response.headers["content-type"] || ""
|
||||||
const mime = mimeFrom(contentType)
|
const mime = mimeFrom(contentType)
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ The current year is ${new Date().getFullYear()}. Use this year when searching fo
|
|||||||
|
|
||||||
export const Parameters = Schema.Struct({
|
export const Parameters = Schema.Struct({
|
||||||
query: Schema.String.annotate({ description: "Websearch query" }),
|
query: Schema.String.annotate({ description: "Websearch query" }),
|
||||||
numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({ description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})` }),
|
numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({
|
||||||
|
description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})`,
|
||||||
|
}),
|
||||||
livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
|
livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
|
||||||
description:
|
description:
|
||||||
"Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
|
"Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
|
||||||
@@ -42,9 +44,11 @@ export const Parameters = Schema.Struct({
|
|||||||
type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
|
type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
|
||||||
description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
|
description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
|
||||||
}),
|
}),
|
||||||
contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate({
|
contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate(
|
||||||
|
{
|
||||||
description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`,
|
description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`,
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Provider = Schema.Literals(["exa", "parallel"])
|
export const Provider = Schema.Literals(["exa", "parallel"])
|
||||||
@@ -63,13 +67,11 @@ export class ConfigService extends Context.Service<ConfigService, Config>()("@op
|
|||||||
/** Isolates the retained product environment contract from the generic tool implementation. */
|
/** Isolates the retained product environment contract from the generic tool implementation. */
|
||||||
export const defaultConfigLayer = Layer.sync(ConfigService, () =>
|
export const defaultConfigLayer = Layer.sync(ConfigService, () =>
|
||||||
ConfigService.of({
|
ConfigService.of({
|
||||||
provider: process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
|
provider:
|
||||||
|
process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
|
||||||
? process.env.OPENCODE_WEBSEARCH_PROVIDER
|
? process.env.OPENCODE_WEBSEARCH_PROVIDER
|
||||||
: undefined,
|
: undefined,
|
||||||
enableExa:
|
enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"),
|
||||||
truthy("OPENCODE_EXPERIMENTAL") ||
|
|
||||||
truthy("OPENCODE_ENABLE_EXA") ||
|
|
||||||
truthy("OPENCODE_EXPERIMENTAL_EXA"),
|
|
||||||
enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
|
enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
|
||||||
exaApiKey: process.env.EXA_API_KEY,
|
exaApiKey: process.env.EXA_API_KEY,
|
||||||
parallelApiKey: process.env.PARALLEL_API_KEY,
|
parallelApiKey: process.env.PARALLEL_API_KEY,
|
||||||
@@ -162,9 +164,15 @@ const callMcp = <F extends Schema.Struct.Fields>(
|
|||||||
return yield* Effect.gen(function* () {
|
return yield* Effect.gen(function* () {
|
||||||
const response = yield* HttpClient.filterStatusOk(http).execute(request)
|
const response = yield* HttpClient.filterStatusOk(http).execute(request)
|
||||||
const body = yield* response.text
|
const body = yield* response.text
|
||||||
if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES) return yield* Effect.die(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`))
|
if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES)
|
||||||
|
return yield* Effect.die(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`))
|
||||||
return yield* parseResponse(body)
|
return yield* parseResponse(body)
|
||||||
}).pipe(Effect.timeoutOrElse({ duration: Duration.seconds(25), orElse: () => Effect.die(new Error(`${tool} request timed out`)) }))
|
}).pipe(
|
||||||
|
Effect.timeoutOrElse({
|
||||||
|
duration: Duration.seconds(25),
|
||||||
|
orElse: () => Effect.die(new Error(`${tool} request timed out`)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const Success = Schema.Struct({
|
const Success = Schema.Struct({
|
||||||
@@ -201,7 +209,8 @@ export const layer = Layer.effectDiscard(
|
|||||||
metadata: { ...parameters, provider },
|
metadata: { ...parameters, provider },
|
||||||
})
|
})
|
||||||
|
|
||||||
const text = provider === "exa"
|
const text =
|
||||||
|
provider === "exa"
|
||||||
? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, {
|
? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, {
|
||||||
query: parameters.query,
|
query: parameters.query,
|
||||||
type: parameters.type || "auto",
|
type: parameters.type || "auto",
|
||||||
@@ -234,7 +243,12 @@ export const layer = Layer.effectDiscard(
|
|||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchCause((cause) =>
|
Effect.catchCause((cause) =>
|
||||||
Effect.fail(new ToolFailure({ message: `Unable to search the web for ${parameters.query}`, error: Cause.squash(cause) })),
|
Effect.fail(
|
||||||
|
new ToolFailure({
|
||||||
|
message: `Unable to search the web for ${parameters.query}`,
|
||||||
|
error: Cause.squash(cause),
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -114,7 +114,15 @@ describe("AgentV2", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const agents = yield* agent.all()
|
const agents = yield* agent.all()
|
||||||
expect(agents.map((item) => String(item.id)).sort()).toEqual(["build", "compaction", "explore", "general", "plan", "summary", "title"])
|
expect(agents.map((item) => String(item.id)).sort()).toEqual([
|
||||||
|
"build",
|
||||||
|
"compaction",
|
||||||
|
"explore",
|
||||||
|
"general",
|
||||||
|
"plan",
|
||||||
|
"summary",
|
||||||
|
"title",
|
||||||
|
])
|
||||||
for (const item of agents) {
|
for (const item of agents) {
|
||||||
expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false)
|
expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ describe("DatabaseMigration", () => {
|
|||||||
const layers = [Database.layerFromPath(filename), Database.layerFromPath(filename)]
|
const layers = [Database.layerFromPath(filename), Database.layerFromPath(filename)]
|
||||||
|
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
Effect.all(layers.map((layer) => Effect.scoped(Layer.build(layer))), { concurrency: "unbounded" }),
|
Effect.all(
|
||||||
|
layers.map((layer) => Effect.scoped(Layer.build(layer))),
|
||||||
|
{ concurrency: "unbounded" },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ describe("KeyedMutex", () => {
|
|||||||
const secondStarted = yield* Deferred.make<void>()
|
const secondStarted = yield* Deferred.make<void>()
|
||||||
|
|
||||||
const first = yield* mutex
|
const first = yield* mutex
|
||||||
.withLock("shared")(Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))))
|
.withLock("shared")(
|
||||||
|
Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))),
|
||||||
|
)
|
||||||
.pipe(Effect.forkChild)
|
.pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(firstStarted)
|
yield* Deferred.await(firstStarted)
|
||||||
const second = yield* mutex.withLock("shared")(Deferred.succeed(secondStarted, undefined)).pipe(Effect.forkChild)
|
const second = yield* mutex.withLock("shared")(Deferred.succeed(secondStarted, undefined)).pipe(Effect.forkChild)
|
||||||
@@ -53,7 +55,9 @@ describe("KeyedMutex", () => {
|
|||||||
const releaseFirst = yield* Deferred.make<void>()
|
const releaseFirst = yield* Deferred.make<void>()
|
||||||
|
|
||||||
const first = yield* mutex
|
const first = yield* mutex
|
||||||
.withLock("shared")(Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))))
|
.withLock("shared")(
|
||||||
|
Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))),
|
||||||
|
)
|
||||||
.pipe(Effect.forkChild)
|
.pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(firstStarted)
|
yield* Deferred.await(firstStarted)
|
||||||
const interrupted = yield* mutex.withLock("shared")(Effect.void).pipe(Effect.forkChild)
|
const interrupted = yield* mutex.withLock("shared")(Effect.void).pipe(Effect.forkChild)
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import { testEffect } from "./lib/effect"
|
|||||||
|
|
||||||
const locationLayer = Layer.succeed(
|
const locationLayer = Layer.succeed(
|
||||||
Location.Service,
|
Location.Service,
|
||||||
Location.Service.of(location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") })),
|
Location.Service.of(
|
||||||
|
location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const eventLayer = Layer.mergeAll(EventV2.defaultLayer, Database.defaultLayer)
|
const eventLayer = Layer.mergeAll(EventV2.defaultLayer, Database.defaultLayer)
|
||||||
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
|
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
|
||||||
@@ -89,7 +91,9 @@ describe("EventV2", () => {
|
|||||||
expect(EventV2.ID.fromExternal(input)).toBe(EventV2.ID.fromExternal(input))
|
expect(EventV2.ID.fromExternal(input)).toBe(EventV2.ID.fromExternal(input))
|
||||||
expect(EventV2.ID.fromExternal(input)).toMatch(/^evt_[a-f0-9]{64}$/)
|
expect(EventV2.ID.fromExternal(input)).toMatch(/^evt_[a-f0-9]{64}$/)
|
||||||
expect(EventV2.ID.fromExternal({ ...input, namespace: "another-app" })).not.toBe(EventV2.ID.fromExternal(input))
|
expect(EventV2.ID.fromExternal({ ...input, namespace: "another-app" })).not.toBe(EventV2.ID.fromExternal(input))
|
||||||
expect(EventV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe(EventV2.ID.fromExternal({ namespace: "a", key: "b:c" }))
|
expect(EventV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe(
|
||||||
|
EventV2.ID.fromExternal({ namespace: "a", key: "b:c" }),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,7 +109,10 @@ describe("EventV2", () => {
|
|||||||
expect(event.type).toBe("test.message")
|
expect(event.type).toBe("test.message")
|
||||||
expect(event).not.toHaveProperty("version")
|
expect(event).not.toHaveProperty("version")
|
||||||
expect(event.data).toEqual({ text: "hello" })
|
expect(event.data).toEqual({ text: "hello" })
|
||||||
expect(event.location).toEqual({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") })
|
expect(event.location).toEqual({
|
||||||
|
directory: AbsolutePath.make("project"),
|
||||||
|
workspaceID: WorkspaceV2.ID.make("wrk_test"),
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,7 +299,9 @@ describe("EventV2", () => {
|
|||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
||||||
const fiber = yield* events.aggregateEvents({ aggregateID, after: EventV2.Cursor.make(0) }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID, after: EventV2.Cursor.make(0) })
|
||||||
|
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
|
||||||
@@ -309,11 +318,18 @@ describe("EventV2", () => {
|
|||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
|
||||||
const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID })
|
||||||
|
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
||||||
|
|
||||||
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
|
||||||
|
|
||||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, (event.event.data as { text: string }).text])).toEqual([
|
expect(
|
||||||
|
Array.from(yield* Fiber.join(fiber)).map((event) => [
|
||||||
|
event.cursor,
|
||||||
|
(event.event.data as { text: string }).text,
|
||||||
|
]),
|
||||||
|
).toEqual([
|
||||||
[EventV2.Cursor.make(0), "zero"],
|
[EventV2.Cursor.make(0), "zero"],
|
||||||
[EventV2.Cursor.make(1), "one"],
|
[EventV2.Cursor.make(1), "one"],
|
||||||
])
|
])
|
||||||
@@ -336,7 +352,9 @@ describe("EventV2", () => {
|
|||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID })
|
||||||
|
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Deferred.await(readStarted)
|
yield* Deferred.await(readStarted)
|
||||||
|
|
||||||
pause = false
|
pause = false
|
||||||
@@ -355,7 +373,9 @@ describe("EventV2", () => {
|
|||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
const count = 64
|
const count = 64
|
||||||
const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID })
|
||||||
|
.pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
for (let index = 0; index < count; index++) {
|
for (let index = 0; index < count; index++) {
|
||||||
@@ -363,7 +383,10 @@ describe("EventV2", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual(
|
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual(
|
||||||
Array.from({ length: count }, (_, index) => [EventV2.Cursor.make(index), { id: aggregateID, text: String(index) }]),
|
Array.from({ length: count }, (_, index) => [
|
||||||
|
EventV2.Cursor.make(index),
|
||||||
|
{ id: aggregateID, text: String(index) },
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -372,7 +395,9 @@ describe("EventV2", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const aggregateID = EventV2.ID.create()
|
const aggregateID = EventV2.ID.create()
|
||||||
const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
const fiber = yield* events
|
||||||
|
.aggregateEvents({ aggregateID })
|
||||||
|
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||||
yield* Effect.yieldNow
|
yield* Effect.yieldNow
|
||||||
|
|
||||||
yield* events.publish(Message, { text: "live only" })
|
yield* events.publish(Message, { text: "live only" })
|
||||||
@@ -450,7 +475,9 @@ describe("EventV2", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate", () =>
|
it.effect(
|
||||||
|
"replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate",
|
||||||
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
@@ -750,7 +777,8 @@ describe("EventV2", () => {
|
|||||||
{ ownerID: "owner-1" },
|
{ ownerID: "owner-1" },
|
||||||
)
|
)
|
||||||
|
|
||||||
const exit = yield* events.replay(
|
const exit = yield* events
|
||||||
|
.replay(
|
||||||
{
|
{
|
||||||
id: EventV2.ID.create(),
|
id: EventV2.ID.create(),
|
||||||
type: EventV2.versionedType(SyncMessage.type, 1),
|
type: EventV2.versionedType(SyncMessage.type, 1),
|
||||||
@@ -759,7 +787,8 @@ describe("EventV2", () => {
|
|||||||
data: { id: aggregateID, text: "conflict" },
|
data: { id: aggregateID, text: "conflict" },
|
||||||
},
|
},
|
||||||
{ ownerID: "owner-2", strictOwner: true },
|
{ ownerID: "owner-2", strictOwner: true },
|
||||||
).pipe(Effect.exit)
|
)
|
||||||
|
.pipe(Effect.exit)
|
||||||
|
|
||||||
expect(String(exit)).toContain("Replay owner mismatch")
|
expect(String(exit)).toContain("Replay owner mismatch")
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ describe("FileMutation", () => {
|
|||||||
const plan = yield* (yield* LocationMutation.Service).resolve({ path: "appeared.txt" })
|
const plan = yield* (yield* LocationMutation.Service).resolve({ path: "appeared.txt" })
|
||||||
yield* Effect.promise(() => fs.writeFile(targetPath, "winner"))
|
yield* Effect.promise(() => fs.writeFile(targetPath, "winner"))
|
||||||
|
|
||||||
expect(yield* (yield* FileMutation.Service).create({ plan, content: "replacement" }).pipe(Effect.flip)).toMatchObject({
|
expect(
|
||||||
|
yield* (yield* FileMutation.Service).create({ plan, content: "replacement" }).pipe(Effect.flip),
|
||||||
|
).toMatchObject({
|
||||||
_tag: "LocationMutation.RevalidationError",
|
_tag: "LocationMutation.RevalidationError",
|
||||||
})
|
})
|
||||||
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("winner")
|
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("winner")
|
||||||
@@ -105,8 +107,20 @@ describe("FileMutation", () => {
|
|||||||
const plan = yield* (yield* LocationMutation.Service).resolve({ path: "remove.txt" })
|
const plan = yield* (yield* LocationMutation.Service).resolve({ path: "remove.txt" })
|
||||||
const result = yield* (yield* FileMutation.Service).remove({ plan })
|
const result = yield* (yield* FileMutation.Service).remove({ plan })
|
||||||
|
|
||||||
expect(result).toEqual({ operation: "remove", target: plan.target.canonical, resource: "remove.txt", existed: true })
|
expect(result).toEqual({
|
||||||
expect(yield* Effect.promise(() => fs.stat(targetPath).then(() => true, () => false))).toBe(false)
|
operation: "remove",
|
||||||
|
target: plan.target.canonical,
|
||||||
|
resource: "remove.txt",
|
||||||
|
existed: true,
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
fs.stat(targetPath).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toBe(false)
|
||||||
}).pipe(provide(directory)),
|
}).pipe(provide(directory)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -119,7 +133,12 @@ describe("FileMutation", () => {
|
|||||||
const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
||||||
const result = yield* (yield* FileMutation.Service).write({ plan, content: "external" })
|
const result = yield* (yield* FileMutation.Service).write({ plan, content: "external" })
|
||||||
|
|
||||||
expect(result).toEqual({ operation: "write", target: plan.target.canonical, resource: plan.target.resource, existed: false })
|
expect(result).toEqual({
|
||||||
|
operation: "write",
|
||||||
|
target: plan.target.canonical,
|
||||||
|
resource: plan.target.resource,
|
||||||
|
existed: false,
|
||||||
|
})
|
||||||
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("external")
|
expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("external")
|
||||||
}).pipe(provide(directory)),
|
}).pipe(provide(directory)),
|
||||||
),
|
),
|
||||||
@@ -135,8 +154,20 @@ describe("FileMutation", () => {
|
|||||||
const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
||||||
const result = yield* (yield* FileMutation.Service).remove({ plan })
|
const result = yield* (yield* FileMutation.Service).remove({ plan })
|
||||||
|
|
||||||
expect(result).toEqual({ operation: "remove", target: plan.target.canonical, resource: plan.target.resource, existed: true })
|
expect(result).toEqual({
|
||||||
expect(yield* Effect.promise(() => fs.stat(targetPath).then(() => true, () => false))).toBe(false)
|
operation: "remove",
|
||||||
|
target: plan.target.canonical,
|
||||||
|
resource: plan.target.resource,
|
||||||
|
existed: true,
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
fs.stat(targetPath).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toBe(false)
|
||||||
}).pipe(provide(directory)),
|
}).pipe(provide(directory)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -155,10 +186,19 @@ describe("FileMutation", () => {
|
|||||||
await fs.symlink(outside, parent)
|
await fs.symlink(outside, parent)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(yield* (yield* FileMutation.Service).write({ plan, content: "escape" }).pipe(Effect.flip)).toMatchObject({
|
expect(
|
||||||
|
yield* (yield* FileMutation.Service).write({ plan, content: "escape" }).pipe(Effect.flip),
|
||||||
|
).toMatchObject({
|
||||||
_tag: "LocationMutation.RevalidationError",
|
_tag: "LocationMutation.RevalidationError",
|
||||||
})
|
})
|
||||||
expect(yield* Effect.promise(() => fs.stat(path.join(outside, "new.txt")).then(() => true, () => false))).toBe(false)
|
expect(
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
fs.stat(path.join(outside, "new.txt")).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toBe(false)
|
||||||
}).pipe(provide(directory)),
|
}).pipe(provide(directory)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -233,7 +273,9 @@ describe("FileMutation", () => {
|
|||||||
const expected = new TextEncoder().encode("initial")
|
const expected = new TextEncoder().encode("initial")
|
||||||
const first = yield* files.writeIfUnchanged({ plan, expected, content: "first" }).pipe(Effect.forkChild)
|
const first = yield* files.writeIfUnchanged({ plan, expected, content: "first" }).pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(firstStarted)
|
yield* Deferred.await(firstStarted)
|
||||||
const second = yield* files.writeIfUnchanged({ plan, expected, content: "second" }).pipe(Effect.flip, Effect.forkChild)
|
const second = yield* files
|
||||||
|
.writeIfUnchanged({ plan, expected, content: "second" })
|
||||||
|
.pipe(Effect.flip, Effect.forkChild)
|
||||||
|
|
||||||
yield* Deferred.succeed(releaseFirst, undefined)
|
yield* Deferred.succeed(releaseFirst, undefined)
|
||||||
yield* Fiber.join(first)
|
yield* Fiber.join(first)
|
||||||
|
|||||||
@@ -138,10 +138,9 @@ describe("LocationSearch", () => {
|
|||||||
RelativePath.make("visible.txt"),
|
RelativePath.make("visible.txt"),
|
||||||
])
|
])
|
||||||
expect((yield* search.files({ pattern: ".env" })).items).toEqual([])
|
expect((yield* search.files({ pattern: ".env" })).items).toEqual([])
|
||||||
expect((yield* search.grep({ pattern: "needle", include: "*" })).items.map((item) => item.path).sort()).toEqual([
|
expect((yield* search.grep({ pattern: "needle", include: "*" })).items.map((item) => item.path).sort()).toEqual(
|
||||||
RelativePath.make("nested/visible.txt"),
|
[RelativePath.make("nested/visible.txt"), RelativePath.make("visible.txt")],
|
||||||
RelativePath.make("visible.txt"),
|
)
|
||||||
])
|
|
||||||
}).pipe(provide(directory)),
|
}).pipe(provide(directory)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -191,7 +190,9 @@ describe("LocationSearch", () => {
|
|||||||
it.live("rejects oversized ripgrep JSON records before durable projection", () =>
|
it.live("rejects oversized ripgrep JSON records before durable projection", () =>
|
||||||
withTmp((directory) =>
|
withTmp((directory) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Effect.promise(() => fs.writeFile(path.join(directory, "huge.txt"), `needle ${"x".repeat(Ripgrep.MAX_RECORD_BYTES)}\n`))
|
yield* Effect.promise(() =>
|
||||||
|
fs.writeFile(path.join(directory, "huge.txt"), `needle ${"x".repeat(Ripgrep.MAX_RECORD_BYTES)}\n`),
|
||||||
|
)
|
||||||
const exit = yield* (yield* LocationSearch.Service).grep({ pattern: "needle" }).pipe(Effect.exit)
|
const exit = yield* (yield* LocationSearch.Service).grep({ pattern: "needle" }).pipe(Effect.exit)
|
||||||
|
|
||||||
expect(Exit.isFailure(exit)).toBe(true)
|
expect(Exit.isFailure(exit)).toBe(true)
|
||||||
@@ -243,7 +244,9 @@ describe("LocationSearch", () => {
|
|||||||
await fs.symlink(outside, source)
|
await fs.symlink(outside, source)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(Exit.isFailure(yield* (yield* LocationSearch.Service).files({ pattern: "*" }, approved).pipe(Effect.exit))).toBe(true)
|
expect(
|
||||||
|
Exit.isFailure(yield* (yield* LocationSearch.Service).files({ pattern: "*" }, approved).pipe(Effect.exit)),
|
||||||
|
).toBe(true)
|
||||||
yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true }))
|
yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true }))
|
||||||
}).pipe(provide(directory)),
|
}).pipe(provide(directory)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,10 +4,17 @@ import { Patch } from "@opencode-ai/core/patch"
|
|||||||
describe("Patch", () => {
|
describe("Patch", () => {
|
||||||
test("parses add, update, and delete hunks", () => {
|
test("parses add, update, and delete hunks", () => {
|
||||||
expect(
|
expect(
|
||||||
Patch.parse("*** Begin Patch\n*** Add File: add.txt\n+added\n*** Update File: update.txt\n@@ section\n-old\n+new\n*** Delete File: delete.txt\n*** End Patch"),
|
Patch.parse(
|
||||||
|
"*** Begin Patch\n*** Add File: add.txt\n+added\n*** Update File: update.txt\n@@ section\n-old\n+new\n*** Delete File: delete.txt\n*** End Patch",
|
||||||
|
),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{ type: "add", path: "add.txt", contents: "added" },
|
{ type: "add", path: "add.txt", contents: "added" },
|
||||||
{ type: "update", path: "update.txt", chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: "section", endOfFile: undefined }], movePath: undefined },
|
{
|
||||||
|
type: "update",
|
||||||
|
path: "update.txt",
|
||||||
|
chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: "section", endOfFile: undefined }],
|
||||||
|
movePath: undefined,
|
||||||
|
},
|
||||||
{ type: "delete", path: "delete.txt" },
|
{ type: "delete", path: "delete.txt" },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -19,11 +26,7 @@ describe("Patch", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("derives fuzzy line updates while preserving BOM", () => {
|
test("derives fuzzy line updates while preserving BOM", () => {
|
||||||
const update = Patch.derive(
|
const update = Patch.derive("update.txt", [{ oldLines: [" old "], newLines: ["new"] }], "\uFEFFold\n")
|
||||||
"update.txt",
|
|
||||||
[{ oldLines: [" old "], newLines: ["new"] }],
|
|
||||||
"\uFEFFold\n",
|
|
||||||
)
|
|
||||||
expect(update).toEqual({ content: "new\n", bom: true })
|
expect(update).toEqual({ content: "new\n", bom: true })
|
||||||
expect(Patch.joinBom(update.content, update.bom)).toBe("\uFEFFnew\n")
|
expect(Patch.joinBom(update.content, update.bom)).toBe("\uFEFFnew\n")
|
||||||
})
|
})
|
||||||
@@ -39,7 +42,9 @@ describe("Patch", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("parses the EOF marker inside update chunks", () => {
|
test("parses the EOF marker inside update chunks", () => {
|
||||||
expect(Patch.parse("*** Begin Patch\n*** Update File: update.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch")).toEqual([
|
expect(
|
||||||
|
Patch.parse("*** Begin Patch\n*** Update File: update.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch"),
|
||||||
|
).toEqual([
|
||||||
{
|
{
|
||||||
type: "update",
|
type: "update",
|
||||||
path: "update.txt",
|
path: "update.txt",
|
||||||
@@ -50,8 +55,14 @@ describe("Patch", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("rejects malformed hunk bodies", () => {
|
test("rejects malformed hunk bodies", () => {
|
||||||
expect(() => Patch.parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toThrow("Invalid add file line")
|
expect(() => Patch.parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toThrow(
|
||||||
expect(() => Patch.parse("*** Begin Patch\n*** Update File: update.txt\n*** End Patch")).toThrow("expected at least one @@ chunk")
|
"Invalid add file line",
|
||||||
expect(() => Patch.parse("*** Begin Patch\n*** Delete File: delete.txt\nunexpected body\n*** End Patch")).toThrow("Invalid patch line")
|
)
|
||||||
|
expect(() => Patch.parse("*** Begin Patch\n*** Update File: update.txt\n*** End Patch")).toThrow(
|
||||||
|
"expected at least one @@ chunk",
|
||||||
|
)
|
||||||
|
expect(() => Patch.parse("*** Begin Patch\n*** Delete File: delete.txt\nunexpected body\n*** End Patch")).toThrow(
|
||||||
|
"Invalid patch line",
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ const current = Layer.succeed(
|
|||||||
)
|
)
|
||||||
const events = EventV2.layer.pipe(Layer.provide(database))
|
const events = EventV2.layer.pipe(Layer.provide(database))
|
||||||
const store = SessionStore.layer.pipe(Layer.provide(database))
|
const store = SessionStore.layer.pipe(Layer.provide(database))
|
||||||
const sessions = SessionV2.layer.pipe(Layer.provide(events), Layer.provide(database), Layer.provide(store), Layer.provide(Project.defaultLayer), Layer.provide(SessionExecution.noopLayer))
|
const sessions = SessionV2.layer.pipe(
|
||||||
|
Layer.provide(events),
|
||||||
|
Layer.provide(database),
|
||||||
|
Layer.provide(store),
|
||||||
|
Layer.provide(Project.defaultLayer),
|
||||||
|
Layer.provide(SessionExecution.noopLayer),
|
||||||
|
)
|
||||||
const saved = PermissionSaved.layer.pipe(Layer.provide(database))
|
const saved = PermissionSaved.layer.pipe(Layer.provide(database))
|
||||||
const layer = PermissionV2.locationLayer.pipe(
|
const layer = PermissionV2.locationLayer.pipe(
|
||||||
Layer.provideMerge(database),
|
Layer.provideMerge(database),
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ describe("SessionV2.create", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
const created = yield* session.create({ location })
|
const created = yield* session.create({ location })
|
||||||
const unavailable = (effect: Effect.Effect<void, SessionV2.NotFoundError | SessionV2.OperationUnavailableError>) =>
|
const unavailable = (
|
||||||
|
effect: Effect.Effect<void, SessionV2.NotFoundError | SessionV2.OperationUnavailableError>,
|
||||||
|
) =>
|
||||||
effect.pipe(
|
effect.pipe(
|
||||||
Effect.flip,
|
Effect.flip,
|
||||||
Effect.map((error) => (error instanceof SessionV2.OperationUnavailableError ? error.operation : "not-found")),
|
Effect.map((error) => (error instanceof SessionV2.OperationUnavailableError ? error.operation : "not-found")),
|
||||||
|
|||||||
@@ -87,19 +87,15 @@ describe("SessionProjector", () => {
|
|||||||
})
|
})
|
||||||
expect(secondPage.map((message) => (message.type === "user" ? message.text : message.type))).toEqual(["second"])
|
expect(secondPage.map((message) => (message.type === "user" ? message.text : message.type))).toEqual(["second"])
|
||||||
expect(
|
expect(
|
||||||
(
|
(yield* sessions.messages({
|
||||||
yield* sessions.messages({
|
|
||||||
sessionID,
|
sessionID,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
order: "asc",
|
order: "asc",
|
||||||
cursor: { id: secondPage[0]!.id, direction: "previous" },
|
cursor: { id: secondPage[0]!.id, direction: "previous" },
|
||||||
})
|
})).map((message) => (message.type === "user" ? message.text : message.type)),
|
||||||
).map((message) => (message.type === "user" ? message.text : message.type)),
|
|
||||||
).toEqual(["first"])
|
).toEqual(["first"])
|
||||||
expect(
|
expect(
|
||||||
(yield* sessions.context(sessionID)).map((message) =>
|
(yield* sessions.context(sessionID)).map((message) => (message.type === "user" ? message.text : message.type)),
|
||||||
message.type === "user" ? message.text : message.type,
|
|
||||||
),
|
|
||||||
).toEqual(["first", "second"])
|
).toEqual(["first", "second"])
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
@@ -222,7 +218,9 @@ describe("SessionProjector", () => {
|
|||||||
summary: "summary",
|
summary: "summary",
|
||||||
include: "msg-1",
|
include: "msg-1",
|
||||||
})
|
})
|
||||||
expect(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie)).toMatchObject({
|
expect(
|
||||||
|
yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie),
|
||||||
|
).toMatchObject({
|
||||||
agent: "build",
|
agent: "build",
|
||||||
model,
|
model,
|
||||||
time_updated: DateTime.toEpochMillis(created),
|
time_updated: DateTime.toEpochMillis(created),
|
||||||
@@ -272,17 +270,36 @@ describe("SessionProjector", () => {
|
|||||||
it.effect("rejects a Prompted delivery mode that conflicts with an admitted inbox row", () =>
|
it.effect("rejects a Prompted delivery mode that conflicts with an admitted inbox row", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
yield* db.insert(ProjectTable).values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }).run().pipe(Effect.orDie)
|
yield* db
|
||||||
yield* db.insert(SessionTable).values({ id: sessionID, project_id: Project.ID.global, slug: "test", directory: "/project", title: "test", version: "test" }).run().pipe(Effect.orDie)
|
.insert(ProjectTable)
|
||||||
|
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
yield* db
|
||||||
|
.insert(SessionTable)
|
||||||
|
.values({
|
||||||
|
id: sessionID,
|
||||||
|
project_id: Project.ID.global,
|
||||||
|
slug: "test",
|
||||||
|
directory: "/project",
|
||||||
|
title: "test",
|
||||||
|
version: "test",
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const id = SessionMessage.ID.make("evt_delivery_conflict")
|
const id = SessionMessage.ID.make("evt_delivery_conflict")
|
||||||
const prompt = new Prompt({ text: "admitted" })
|
const prompt = new Prompt({ text: "admitted" })
|
||||||
yield* SessionInput.admit(db, { id, sessionID, prompt, delivery: "queue" })
|
yield* SessionInput.admit(db, { id, sessionID, prompt, delivery: "queue" })
|
||||||
|
|
||||||
const exit = yield* events.publish(SessionEvent.Prompted, { sessionID, timestamp: created, prompt, delivery: "steer" }, { id }).pipe(Effect.exit)
|
const exit = yield* events
|
||||||
|
.publish(SessionEvent.Prompted, { sessionID, timestamp: created, prompt, delivery: "steer" }, { id })
|
||||||
|
.pipe(Effect.exit)
|
||||||
|
|
||||||
expect(String(exit)).toContain("Prompt projection conflicts with admitted input")
|
expect(String(exit)).toContain("Prompt projection conflicts with admitted input")
|
||||||
expect(yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie)).toMatchObject({ delivery: "queue", promoted_seq: null })
|
expect(
|
||||||
|
yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie),
|
||||||
|
).toMatchObject({ delivery: "queue", promoted_seq: null })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,9 @@ describe("SessionV2.prompt", () => {
|
|||||||
yield* SessionInput.promoteSteers(db, events, sessionID)
|
yield* SessionInput.promoteSteers(db, events, sessionID)
|
||||||
|
|
||||||
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 0 })
|
expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 0 })
|
||||||
expect(yield* session.messages({ sessionID })).toMatchObject([{ id: messageID, type: "user", text: "Reserved prompt" }])
|
expect(yield* session.messages({ sessionID })).toMatchObject([
|
||||||
|
{ id: messageID, type: "user", text: "Reserved prompt" },
|
||||||
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,14 @@ describe("toLLMMessages", () => {
|
|||||||
test("maps every top-level V2 Session message type", () => {
|
test("maps every top-level V2 Session message type", () => {
|
||||||
const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
|
const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
|
||||||
const reference = new ReferenceAttachment({ name: "docs", kind: "local", uri: "file:///docs" })
|
const reference = new ReferenceAttachment({ name: "docs", kind: "local", uri: "file:///docs" })
|
||||||
const messages = toLLMMessages([
|
const messages = toLLMMessages(
|
||||||
new SessionMessage.AgentSwitched({ id: id("agent"), type: "agent-switched", agent: "build", time: { created } }),
|
[
|
||||||
|
new SessionMessage.AgentSwitched({
|
||||||
|
id: id("agent"),
|
||||||
|
type: "agent-switched",
|
||||||
|
agent: "build",
|
||||||
|
time: { created },
|
||||||
|
}),
|
||||||
new SessionMessage.ModelSwitched({
|
new SessionMessage.ModelSwitched({
|
||||||
id: id("model"),
|
id: id("model"),
|
||||||
type: "model-switched",
|
type: "model-switched",
|
||||||
@@ -58,7 +64,9 @@ describe("toLLMMessages", () => {
|
|||||||
summary: "Earlier work",
|
summary: "Earlier work",
|
||||||
time: { created },
|
time: { created },
|
||||||
}),
|
}),
|
||||||
], model)
|
],
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
|
||||||
expect(messages.map((message) => message.role)).toEqual(["user", "user", "user", "user"])
|
expect(messages.map((message) => message.role)).toEqual(["user", "user", "user", "user"])
|
||||||
expect(messages[0]).toEqual(
|
expect(messages[0]).toEqual(
|
||||||
@@ -80,7 +88,8 @@ describe("toLLMMessages", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("expands assistant tool calls and settled outcomes into canonical tool messages", () => {
|
test("expands assistant tool calls and settled outcomes into canonical tool messages", () => {
|
||||||
const messages = toLLMMessages([
|
const messages = toLLMMessages(
|
||||||
|
[
|
||||||
new SessionMessage.Assistant({
|
new SessionMessage.Assistant({
|
||||||
id: id("assistant"),
|
id: id("assistant"),
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
@@ -167,7 +176,9 @@ describe("toLLMMessages", () => {
|
|||||||
],
|
],
|
||||||
time: { created, completed: created },
|
time: { created, completed: created },
|
||||||
}),
|
}),
|
||||||
], model)
|
],
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
|
||||||
expect(messages.map((message) => message.role)).toEqual(["assistant", "tool"])
|
expect(messages.map((message) => message.role)).toEqual(["assistant", "tool"])
|
||||||
expect(messages[0]?.content).toEqual([
|
expect(messages[0]?.content).toEqual([
|
||||||
@@ -211,7 +222,10 @@ describe("toLLMMessages", () => {
|
|||||||
name: "write",
|
name: "write",
|
||||||
providerExecuted: true,
|
providerExecuted: true,
|
||||||
providerMetadata: { fake: { continuation: "failed" } },
|
providerMetadata: { fake: { continuation: "failed" } },
|
||||||
result: { type: "error", value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} } },
|
result: {
|
||||||
|
type: "error",
|
||||||
|
value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
expect(messages[1]?.content).toEqual([
|
expect(messages[1]?.content).toEqual([
|
||||||
@@ -231,7 +245,8 @@ describe("toLLMMessages", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("restores OpenAI encrypted reasoning metadata", () => {
|
test("restores OpenAI encrypted reasoning metadata", () => {
|
||||||
const messages = toLLMMessages([
|
const messages = toLLMMessages(
|
||||||
|
[
|
||||||
new SessionMessage.Assistant({
|
new SessionMessage.Assistant({
|
||||||
id: id("assistant-openai-reasoning"),
|
id: id("assistant-openai-reasoning"),
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
@@ -247,7 +262,9 @@ describe("toLLMMessages", () => {
|
|||||||
],
|
],
|
||||||
time: { created, completed: created },
|
time: { created, completed: created },
|
||||||
}),
|
}),
|
||||||
], model)
|
],
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
|
||||||
expect(messages[0]?.content).toEqual([
|
expect(messages[0]?.content).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ const permission = Layer.succeed(
|
|||||||
PermissionV2.Service.of({
|
PermissionV2.Service.of({
|
||||||
assert: (input) =>
|
assert: (input) =>
|
||||||
Effect.sync(() => assertions.push(input)).pipe(
|
Effect.sync(() => assertions.push(input)).pipe(
|
||||||
Effect.andThen(input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
|
Effect.andThen(
|
||||||
|
input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ask: () => Effect.die("unused"),
|
ask: () => Effect.die("unused"),
|
||||||
reply: () => Effect.die("unused"),
|
reply: () => Effect.die("unused"),
|
||||||
|
|||||||
@@ -1979,7 +1979,11 @@ describe("SessionRunnerLLM", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool on raw failure" }), resume: false })
|
yield* session.prompt({
|
||||||
|
sessionID,
|
||||||
|
prompt: new Prompt({ text: "Fail hosted tool on raw failure" }),
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
const failure = providerUnavailable()
|
const failure = providerUnavailable()
|
||||||
responseStream = Stream.concat(
|
responseStream = Stream.concat(
|
||||||
Stream.fromIterable([
|
Stream.fromIterable([
|
||||||
|
|||||||
@@ -32,36 +32,125 @@ describe("Tool.Progress", () => {
|
|||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const service = yield* EventV2.Service
|
const service = yield* EventV2.Service
|
||||||
const sessionID = SessionV2.ID.make("ses_tool_progress_projector")
|
const sessionID = SessionV2.ID.make("ses_tool_progress_projector")
|
||||||
yield* db.insert(ProjectTable).values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }).onConflictDoNothing().run().pipe(Effect.orDie)
|
yield* db
|
||||||
yield* db.insert(SessionTable).values({ id: sessionID, project_id: Project.ID.global, slug: "progress", directory: "/project", title: "progress", version: "test" }).run().pipe(Effect.orDie)
|
.insert(ProjectTable)
|
||||||
const assistantMessageID = (yield* service.publish(SessionEvent.Step.Started, { sessionID, timestamp, agent: "build", model })).id
|
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
|
||||||
|
.onConflictDoNothing()
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
yield* db
|
||||||
|
.insert(SessionTable)
|
||||||
|
.values({
|
||||||
|
id: sessionID,
|
||||||
|
project_id: Project.ID.global,
|
||||||
|
slug: "progress",
|
||||||
|
directory: "/project",
|
||||||
|
title: "progress",
|
||||||
|
version: "test",
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
|
const assistantMessageID = (yield* service.publish(SessionEvent.Step.Started, {
|
||||||
|
sessionID,
|
||||||
|
timestamp,
|
||||||
|
agent: "build",
|
||||||
|
model,
|
||||||
|
})).id
|
||||||
const readAssistant = Effect.gen(function* () {
|
const readAssistant = Effect.gen(function* () {
|
||||||
const row = yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, assistantMessageID)).get().pipe(Effect.orDie)
|
const row = yield* db
|
||||||
|
.select()
|
||||||
|
.from(SessionMessageTable)
|
||||||
|
.where(eq(SessionMessageTable.id, assistantMessageID))
|
||||||
|
.get()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
if (!row) return yield* Effect.die("Missing projected assistant")
|
if (!row) return yield* Effect.die("Missing projected assistant")
|
||||||
return Schema.decodeUnknownSync(SessionMessage.Assistant)({ ...row.data, id: row.id, type: row.type })
|
return Schema.decodeUnknownSync(SessionMessage.Assistant)({ ...row.data, id: row.id, type: row.type })
|
||||||
})
|
})
|
||||||
const start = (callID: string) => Effect.gen(function* () {
|
const start = (callID: string) =>
|
||||||
yield* service.publish(SessionEvent.Tool.Input.Started, { sessionID, timestamp, assistantMessageID, callID, name: "bash" })
|
Effect.gen(function* () {
|
||||||
yield* service.publish(SessionEvent.Tool.Called, { sessionID, timestamp, assistantMessageID, callID, tool: "bash", input: { command: "pwd" }, provider: { executed: false } })
|
yield* service.publish(SessionEvent.Tool.Input.Started, {
|
||||||
|
sessionID,
|
||||||
|
timestamp,
|
||||||
|
assistantMessageID,
|
||||||
|
callID,
|
||||||
|
name: "bash",
|
||||||
|
})
|
||||||
|
yield* service.publish(SessionEvent.Tool.Called, {
|
||||||
|
sessionID,
|
||||||
|
timestamp,
|
||||||
|
assistantMessageID,
|
||||||
|
callID,
|
||||||
|
tool: "bash",
|
||||||
|
input: { command: "pwd" },
|
||||||
|
provider: { executed: false },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* start("call-success")
|
yield* start("call-success")
|
||||||
expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "running", structured: {}, content: [] } })
|
expect((yield* readAssistant).content[0]).toMatchObject({
|
||||||
|
state: { status: "running", structured: {}, content: [] },
|
||||||
|
})
|
||||||
|
|
||||||
yield* service.publish(SessionEvent.Tool.Progress, { sessionID, timestamp, assistantMessageID, callID: "call-success", structured: { phase: "checkpoint" }, content: content("saved") })
|
yield* service.publish(SessionEvent.Tool.Progress, {
|
||||||
expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "running", structured: { phase: "checkpoint" }, content: content("saved") } })
|
sessionID,
|
||||||
|
timestamp,
|
||||||
|
assistantMessageID,
|
||||||
|
callID: "call-success",
|
||||||
|
structured: { phase: "checkpoint" },
|
||||||
|
content: content("saved"),
|
||||||
|
})
|
||||||
|
expect((yield* readAssistant).content[0]).toMatchObject({
|
||||||
|
state: { status: "running", structured: { phase: "checkpoint" }, content: content("saved") },
|
||||||
|
})
|
||||||
|
|
||||||
const success = yield* service.publish(SessionEvent.Tool.Success, { sessionID, timestamp, assistantMessageID, callID: "call-success", structured: { phase: "done" }, content: content("complete"), provider: { executed: false } })
|
const success = yield* service.publish(SessionEvent.Tool.Success, {
|
||||||
expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "completed", structured: { phase: "done" }, content: content("complete") } })
|
sessionID,
|
||||||
|
timestamp,
|
||||||
|
assistantMessageID,
|
||||||
|
callID: "call-success",
|
||||||
|
structured: { phase: "done" },
|
||||||
|
content: content("complete"),
|
||||||
|
provider: { executed: false },
|
||||||
|
})
|
||||||
|
expect((yield* readAssistant).content[0]).toMatchObject({
|
||||||
|
state: { status: "completed", structured: { phase: "done" }, content: content("complete") },
|
||||||
|
})
|
||||||
|
|
||||||
yield* start("call-failed")
|
yield* start("call-failed")
|
||||||
yield* service.publish(SessionEvent.Tool.Progress, { sessionID, timestamp, assistantMessageID, callID: "call-failed", structured: { phase: "checkpoint" }, content: content("before failure") })
|
yield* service.publish(SessionEvent.Tool.Progress, {
|
||||||
const failed = yield* service.publish(SessionEvent.Tool.Failed, { sessionID, timestamp, assistantMessageID, callID: "call-failed", error: { type: "unknown", message: "boom" }, provider: { executed: false } })
|
sessionID,
|
||||||
expect((yield* readAssistant).content[1]).toMatchObject({ state: { status: "error", structured: { phase: "checkpoint" }, content: content("before failure"), error: { type: "unknown", message: "boom" } } })
|
timestamp,
|
||||||
|
assistantMessageID,
|
||||||
|
callID: "call-failed",
|
||||||
|
structured: { phase: "checkpoint" },
|
||||||
|
content: content("before failure"),
|
||||||
|
})
|
||||||
|
const failed = yield* service.publish(SessionEvent.Tool.Failed, {
|
||||||
|
sessionID,
|
||||||
|
timestamp,
|
||||||
|
assistantMessageID,
|
||||||
|
callID: "call-failed",
|
||||||
|
error: { type: "unknown", message: "boom" },
|
||||||
|
provider: { executed: false },
|
||||||
|
})
|
||||||
|
expect((yield* readAssistant).content[1]).toMatchObject({
|
||||||
|
state: {
|
||||||
|
status: "error",
|
||||||
|
structured: { phase: "checkpoint" },
|
||||||
|
content: content("before failure"),
|
||||||
|
error: { type: "unknown", message: "boom" },
|
||||||
|
},
|
||||||
|
})
|
||||||
expect(Schema.is(SessionEvent.Durable)(success)).toBe(true)
|
expect(Schema.is(SessionEvent.Durable)(success)).toBe(true)
|
||||||
expect(Schema.is(SessionEvent.Durable)(failed)).toBe(true)
|
expect(Schema.is(SessionEvent.Durable)(failed)).toBe(true)
|
||||||
|
|
||||||
const rows = yield* db.select({ type: EventTable.type }).from(EventTable).where(eq(EventTable.aggregate_id, sessionID)).orderBy(asc(EventTable.seq)).all().pipe(Effect.orDie)
|
const rows = yield* db
|
||||||
|
.select({ type: EventTable.type })
|
||||||
|
.from(EventTable)
|
||||||
|
.where(eq(EventTable.aggregate_id, sessionID))
|
||||||
|
.orderBy(asc(EventTable.seq))
|
||||||
|
.all()
|
||||||
|
.pipe(Effect.orDie)
|
||||||
expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1))
|
expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1))
|
||||||
expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 1))
|
expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 1))
|
||||||
expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 1))
|
expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 1))
|
||||||
|
|||||||
@@ -86,13 +86,10 @@ describe("SkillDiscovery.pull", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("downloads safe nested files under the skill root", async () => {
|
test("downloads safe nested files under the skill root", async () => {
|
||||||
const result = await pull(
|
const result = await pull([{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], {
|
||||||
[{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }],
|
|
||||||
{
|
|
||||||
[`${base}deploy/SKILL.md`]: "# Deploy",
|
[`${base}deploy/SKILL.md`]: "# Deploy",
|
||||||
[`${base}deploy/references/guide.md`]: "# Guide",
|
[`${base}deploy/references/guide.md`]: "# Guide",
|
||||||
},
|
})
|
||||||
)
|
|
||||||
try {
|
try {
|
||||||
expect(result.directories).toHaveLength(1)
|
expect(result.directories).toHaveLength(1)
|
||||||
expect(result.requests.toSorted()).toEqual(
|
expect(result.requests.toSorted()).toEqual(
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ const permission = Layer.succeed(
|
|||||||
assertions.push(input)
|
assertions.push(input)
|
||||||
if (input.action === "edit") editApproved = true
|
if (input.action === "edit") editApproved = true
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.andThen(input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
|
Effect.andThen(
|
||||||
|
input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ask: () => Effect.die("unused"),
|
ask: () => Effect.die("unused"),
|
||||||
reply: () => Effect.die("unused"),
|
reply: () => Effect.die("unused"),
|
||||||
@@ -67,7 +69,10 @@ const filesystem = Layer.effect(
|
|||||||
remove: (target, options) => {
|
remove: (target, options) => {
|
||||||
if (failRemoveTarget && path.basename(target) === failRemoveTarget) return Effect.die("forced remove failure")
|
if (failRemoveTarget && path.basename(target) === failRemoveTarget) return Effect.die("forced remove failure")
|
||||||
if (blockRemoveTarget && path.basename(target) === blockRemoveTarget && removeStarted && releaseRemove)
|
if (blockRemoveTarget && path.basename(target) === blockRemoveTarget && removeStarted && releaseRemove)
|
||||||
return Deferred.succeed(removeStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseRemove)), Effect.andThen(fs.remove(target, options)))
|
return Deferred.succeed(removeStarted, undefined).pipe(
|
||||||
|
Effect.andThen(Deferred.await(releaseRemove)),
|
||||||
|
Effect.andThen(fs.remove(target, options)),
|
||||||
|
)
|
||||||
return fs.remove(target, options)
|
return fs.remove(target, options)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -98,7 +103,13 @@ const call = (patchText: string, id = "call-apply-patch") => ({
|
|||||||
call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } },
|
call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } },
|
||||||
})
|
})
|
||||||
|
|
||||||
const exists = (target: string) => Effect.promise(() => fs.stat(target).then(() => true, () => false))
|
const exists = (target: string) =>
|
||||||
|
Effect.promise(() =>
|
||||||
|
fs.stat(target).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
),
|
||||||
|
)
|
||||||
const it = testEffect(Layer.empty)
|
const it = testEffect(Layer.empty)
|
||||||
|
|
||||||
describe("ApplyPatchTool", () => {
|
describe("ApplyPatchTool", () => {
|
||||||
@@ -109,13 +120,17 @@ describe("ApplyPatchTool", () => {
|
|||||||
reset()
|
reset()
|
||||||
const update = path.join(tmp.path, "update.txt")
|
const update = path.join(tmp.path, "update.txt")
|
||||||
const remove = path.join(tmp.path, "remove.txt")
|
const remove = path.join(tmp.path, "remove.txt")
|
||||||
return Effect.promise(() => Promise.all([fs.writeFile(update, "before\n"), fs.writeFile(remove, "remove\n")])).pipe(
|
return Effect.promise(() =>
|
||||||
|
Promise.all([fs.writeFile(update, "before\n"), fs.writeFile(remove, "remove\n")]),
|
||||||
|
).pipe(
|
||||||
Effect.andThen(
|
Effect.andThen(
|
||||||
withTool(tmp.path, (registry) =>
|
withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["apply_patch"])
|
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["apply_patch"])
|
||||||
const settled = yield* registry.settle(
|
const settled = yield* registry.settle(
|
||||||
call("*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Update File: update.txt\n@@\n-before\n+after\n*** Delete File: remove.txt\n*** End Patch"),
|
call(
|
||||||
|
"*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Update File: update.txt\n@@\n-before\n+after\n*** Delete File: remove.txt\n*** End Patch",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
expect(settled.result).toEqual({
|
expect(settled.result).toEqual({
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -132,7 +147,9 @@ describe("ApplyPatchTool", () => {
|
|||||||
{ sessionID, action: "edit", resources: ["nested/new.txt", "update.txt", "remove.txt"], save: ["*"] },
|
{ sessionID, action: "edit", resources: ["nested/new.txt", "update.txt", "remove.txt"], save: ["*"] },
|
||||||
])
|
])
|
||||||
expect(readsBeforeEditApproval).toBe(0)
|
expect(readsBeforeEditApproval).toBe(0)
|
||||||
expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "nested/new.txt"), "utf8"))).toBe("created\n")
|
expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "nested/new.txt"), "utf8"))).toBe(
|
||||||
|
"created\n",
|
||||||
|
)
|
||||||
expect(yield* Effect.promise(() => fs.readFile(update, "utf8"))).toBe("after\n")
|
expect(yield* Effect.promise(() => fs.readFile(update, "utf8"))).toBe("after\n")
|
||||||
expect(yield* exists(remove)).toBe(false)
|
expect(yield* exists(remove)).toBe(false)
|
||||||
}),
|
}),
|
||||||
@@ -155,7 +172,11 @@ describe("ApplyPatchTool", () => {
|
|||||||
withTool(tmp.path, (registry) =>
|
withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(
|
expect(
|
||||||
yield* registry.execute(call("*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: old.txt\n*** Move to: moved.txt\n@@\n-before\n+after\n*** End Patch")),
|
yield* registry.execute(
|
||||||
|
call(
|
||||||
|
"*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: old.txt\n*** Move to: moved.txt\n@@\n-before\n+after\n*** End Patch",
|
||||||
|
),
|
||||||
|
),
|
||||||
).toEqual({ type: "error", value: "apply_patch moves are not supported yet" })
|
).toEqual({ type: "error", value: "apply_patch moves are not supported yet" })
|
||||||
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
||||||
expect(assertions).toEqual([])
|
expect(assertions).toEqual([])
|
||||||
@@ -179,7 +200,9 @@ describe("ApplyPatchTool", () => {
|
|||||||
withTool(active.path, (registry) =>
|
withTool(active.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(
|
expect(
|
||||||
yield* registry.execute(call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`)),
|
yield* registry.execute(
|
||||||
|
call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`),
|
||||||
|
),
|
||||||
).toMatchObject({ type: "text" })
|
).toMatchObject({ type: "text" })
|
||||||
expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
|
expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
|
||||||
expect(readsBeforeEditApproval).toBe(0)
|
expect(readsBeforeEditApproval).toBe(0)
|
||||||
@@ -190,7 +213,9 @@ describe("ApplyPatchTool", () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
([active, outside]) =>
|
([active, outside]) =>
|
||||||
Effect.promise(() => Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined)),
|
Effect.promise(() =>
|
||||||
|
Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -201,12 +226,18 @@ describe("ApplyPatchTool", () => {
|
|||||||
reset()
|
reset()
|
||||||
const first = path.join(outside.path, "first.txt")
|
const first = path.join(outside.path, "first.txt")
|
||||||
const second = path.join(outside.path, "second.txt")
|
const second = path.join(outside.path, "second.txt")
|
||||||
return Effect.promise(() => Promise.all([fs.writeFile(first, "before\n"), fs.writeFile(second, "before\n")])).pipe(
|
return Effect.promise(() =>
|
||||||
|
Promise.all([fs.writeFile(first, "before\n"), fs.writeFile(second, "before\n")]),
|
||||||
|
).pipe(
|
||||||
Effect.andThen(
|
Effect.andThen(
|
||||||
withTool(active.path, (registry) =>
|
withTool(active.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(
|
expect(
|
||||||
yield* registry.execute(call(`*** Begin Patch\n*** Update File: ${first}\n@@\n-before\n+after\n*** Update File: ${second}\n@@\n-before\n+after\n*** End Patch`)),
|
yield* registry.execute(
|
||||||
|
call(
|
||||||
|
`*** Begin Patch\n*** Update File: ${first}\n@@\n-before\n+after\n*** Update File: ${second}\n@@\n-before\n+after\n*** End Patch`,
|
||||||
|
),
|
||||||
|
),
|
||||||
).toMatchObject({ type: "text" })
|
).toMatchObject({ type: "text" })
|
||||||
expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
|
expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
|
||||||
expect(assertions[0]?.resources).toEqual([
|
expect(assertions[0]?.resources).toEqual([
|
||||||
@@ -218,7 +249,9 @@ describe("ApplyPatchTool", () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
([active, outside]) =>
|
([active, outside]) =>
|
||||||
Effect.promise(() => Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined)),
|
Effect.promise(() =>
|
||||||
|
Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,7 +263,11 @@ describe("ApplyPatchTool", () => {
|
|||||||
return withTool(tmp.path, (registry) =>
|
return withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(
|
expect(
|
||||||
yield* registry.execute(call("*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: missing.txt\n@@\n-before\n+after\n*** End Patch")),
|
yield* registry.execute(
|
||||||
|
call(
|
||||||
|
"*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: missing.txt\n@@\n-before\n+after\n*** End Patch",
|
||||||
|
),
|
||||||
|
),
|
||||||
).toEqual({ type: "error", value: "Unable to apply patch at missing.txt" })
|
).toEqual({ type: "error", value: "Unable to apply patch at missing.txt" })
|
||||||
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
|
||||||
}),
|
}),
|
||||||
@@ -251,7 +288,9 @@ describe("ApplyPatchTool", () => {
|
|||||||
withTool(tmp.path, (registry) =>
|
withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(
|
expect(
|
||||||
yield* registry.execute(call("*** Begin Patch\n*** Add File: existing.txt\n+replacement\n*** End Patch")),
|
yield* registry.execute(
|
||||||
|
call("*** Begin Patch\n*** Add File: existing.txt\n+replacement\n*** End Patch"),
|
||||||
|
),
|
||||||
).toEqual({ type: "error", value: "Unable to apply patch at existing.txt" })
|
).toEqual({ type: "error", value: "Unable to apply patch at existing.txt" })
|
||||||
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("sentinel\n")
|
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("sentinel\n")
|
||||||
}),
|
}),
|
||||||
@@ -276,8 +315,13 @@ describe("ApplyPatchTool", () => {
|
|||||||
withTool(tmp.path, (registry) =>
|
withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(
|
expect(
|
||||||
yield* registry.execute(call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch")),
|
yield* registry.execute(
|
||||||
).toEqual({ type: "error", value: "Patch partially applied before failing at second.txt. Applied: first.txt" })
|
call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch"),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
type: "error",
|
||||||
|
value: "Patch partially applied before failing at second.txt. Applied: first.txt",
|
||||||
|
})
|
||||||
expect(yield* exists(first)).toBe(false)
|
expect(yield* exists(first)).toBe(false)
|
||||||
expect(yield* exists(second)).toBe(true)
|
expect(yield* exists(second)).toBe(true)
|
||||||
}),
|
}),
|
||||||
@@ -303,7 +347,11 @@ describe("ApplyPatchTool", () => {
|
|||||||
yield* Effect.promise(() => Promise.all([fs.writeFile(first, "first"), fs.writeFile(second, "second")]))
|
yield* Effect.promise(() => Promise.all([fs.writeFile(first, "first"), fs.writeFile(second, "second")]))
|
||||||
yield* withTool(tmp.path, (registry) =>
|
yield* withTool(tmp.path, (registry) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const run = yield* registry.execute(call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch")).pipe(Effect.forkChild)
|
const run = yield* registry
|
||||||
|
.execute(
|
||||||
|
call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch"),
|
||||||
|
)
|
||||||
|
.pipe(Effect.forkChild)
|
||||||
yield* Deferred.await(removeStarted!)
|
yield* Deferred.await(removeStarted!)
|
||||||
const interrupt = yield* Fiber.interrupt(run).pipe(Effect.forkChild)
|
const interrupt = yield* Fiber.interrupt(run).pipe(Effect.forkChild)
|
||||||
yield* Deferred.succeed(releaseRemove!, undefined)
|
yield* Deferred.succeed(releaseRemove!, undefined)
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ const filesystem = Layer.effect(
|
|||||||
return FSUtil.Service.of({
|
return FSUtil.Service.of({
|
||||||
...fs,
|
...fs,
|
||||||
readFile: (target) =>
|
readFile: (target) =>
|
||||||
fs.readFile(target).pipe(
|
fs
|
||||||
|
.readFile(target)
|
||||||
|
.pipe(
|
||||||
Effect.tap((content) =>
|
Effect.tap((content) =>
|
||||||
Effect.sync(() => reads++).pipe(Effect.andThen(Effect.suspend(() => afterRead(target, content)))),
|
Effect.sync(() => reads++).pipe(Effect.andThen(Effect.suspend(() => afterRead(target, content)))),
|
||||||
),
|
),
|
||||||
@@ -369,7 +371,10 @@ describe("EditTool", () => {
|
|||||||
),
|
),
|
||||||
Effect.andThen((result) =>
|
Effect.andThen((result) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(result).toEqual({ type: "error", value: "File changed after permission approval. Read it again before editing." })
|
expect(result).toEqual({
|
||||||
|
type: "error",
|
||||||
|
value: "File changed after permission approval. Read it again before editing.",
|
||||||
|
})
|
||||||
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("newer\n")
|
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("newer\n")
|
||||||
expect(writes).toEqual([])
|
expect(writes).toEqual([])
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -153,7 +153,14 @@ describe("GlobTool", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
reset()
|
reset()
|
||||||
result = new LocationSearch.FilesResult({
|
result = new LocationSearch.FilesResult({
|
||||||
items: [new LocationSearch.File({ path: RelativePath.make("src/index.ts"), canonical: "/project/src/index.ts", resource: "src/index.ts", mtime: 1 })],
|
items: [
|
||||||
|
new LocationSearch.File({
|
||||||
|
path: RelativePath.make("src/index.ts"),
|
||||||
|
canonical: "/project/src/index.ts",
|
||||||
|
resource: "src/index.ts",
|
||||||
|
mtime: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
truncated: false,
|
truncated: false,
|
||||||
partial: false,
|
partial: false,
|
||||||
})
|
})
|
||||||
@@ -172,7 +179,14 @@ describe("GlobTool", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
reset()
|
reset()
|
||||||
result = new LocationSearch.FilesResult({
|
result = new LocationSearch.FilesResult({
|
||||||
items: [new LocationSearch.File({ path: RelativePath.make("guide.md"), canonical: "/project/docs/guide.md", resource: "docs:guide.md", mtime: 1 })],
|
items: [
|
||||||
|
new LocationSearch.File({
|
||||||
|
path: RelativePath.make("guide.md"),
|
||||||
|
canonical: "/project/docs/guide.md",
|
||||||
|
resource: "docs:guide.md",
|
||||||
|
mtime: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
truncated: false,
|
truncated: false,
|
||||||
partial: false,
|
partial: false,
|
||||||
})
|
})
|
||||||
@@ -197,7 +211,14 @@ describe("GlobTool", () => {
|
|||||||
it.effect("formats bounded and partial results without discarding structured output", () =>
|
it.effect("formats bounded and partial results without discarding structured output", () =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const output = new LocationSearch.FilesResult({
|
const output = new LocationSearch.FilesResult({
|
||||||
items: [new LocationSearch.File({ path: RelativePath.make("one.ts"), canonical: "/project/one.ts", resource: "one.ts", mtime: 1 })],
|
items: [
|
||||||
|
new LocationSearch.File({
|
||||||
|
path: RelativePath.make("one.ts"),
|
||||||
|
canonical: "/project/one.ts",
|
||||||
|
resource: "one.ts",
|
||||||
|
mtime: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
truncated: true,
|
truncated: true,
|
||||||
partial: true,
|
partial: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const filesystem = Layer.succeed(
|
|||||||
real: `/project/${input.path ?? "."}`,
|
real: `/project/${input.path ?? "."}`,
|
||||||
directory: "/project",
|
directory: "/project",
|
||||||
root: "/project",
|
root: "/project",
|
||||||
resource: input.reference === undefined ? input.path ?? "." : `${input.reference}:${input.path ?? "."}`,
|
resource: input.reference === undefined ? (input.path ?? ".") : `${input.reference}:${input.path ?? "."}`,
|
||||||
reference: input.reference,
|
reference: input.reference,
|
||||||
type: "directory",
|
type: "directory",
|
||||||
dev: 1,
|
dev: 1,
|
||||||
@@ -87,7 +87,12 @@ const permission = Layer.succeed(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
||||||
const grep = GrepTool.layer.pipe(Layer.provide(registry), Layer.provide(filesystem), Layer.provide(search), Layer.provide(permission))
|
const grep = GrepTool.layer.pipe(
|
||||||
|
Layer.provide(registry),
|
||||||
|
Layer.provide(filesystem),
|
||||||
|
Layer.provide(search),
|
||||||
|
Layer.provide(permission),
|
||||||
|
)
|
||||||
const it = testEffect(Layer.mergeAll(registry, filesystem, search, permission, grep))
|
const it = testEffect(Layer.mergeAll(registry, filesystem, search, permission, grep))
|
||||||
const sessionID = SessionV2.ID.make("ses_grep_tool_test")
|
const sessionID = SessionV2.ID.make("ses_grep_tool_test")
|
||||||
|
|
||||||
@@ -136,7 +141,12 @@ function provideLive(directory: string, projectReferences = references({})) {
|
|||||||
Layer.provide(dependencies),
|
Layer.provide(dependencies),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
|
||||||
const grep = GrepTool.layer.pipe(Layer.provide(registry), Layer.provide(filesystem), Layer.provide(search), Layer.provide(permission))
|
const grep = GrepTool.layer.pipe(
|
||||||
|
Layer.provide(registry),
|
||||||
|
Layer.provide(filesystem),
|
||||||
|
Layer.provide(search),
|
||||||
|
Layer.provide(permission),
|
||||||
|
)
|
||||||
return Layer.mergeAll(registry, filesystem, search, permission, grep)
|
return Layer.mergeAll(registry, filesystem, search, permission, grep)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +188,9 @@ describe("GrepTool", () => {
|
|||||||
resources: ["guide"],
|
resources: ["guide"],
|
||||||
metadata: { root: "manual:docs", reference: "manual", path: RelativePath.make("docs"), include: "*.md" },
|
metadata: { root: "manual:docs", reference: "manual", path: RelativePath.make("docs"), include: "*.md" },
|
||||||
})
|
})
|
||||||
expect(searches).toEqual([{ pattern: "guide", path: RelativePath.make("docs"), reference: "manual", include: "*.md" }])
|
expect(searches).toEqual([
|
||||||
|
{ pattern: "guide", path: RelativePath.make("docs"), reference: "manual", include: "*.md" },
|
||||||
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -218,7 +230,8 @@ describe("GrepTool", () => {
|
|||||||
expect(settlement.output?.structured).toEqual(result)
|
expect(settlement.output?.structured).toEqual(result)
|
||||||
expect(settlement.result).toEqual({
|
expect(settlement.result).toEqual({
|
||||||
type: "text",
|
type: "text",
|
||||||
value: "Found 1 matches\nsrc/index.ts:\n Line 3: needle preview...\n\n(Results are truncated: showing first 1 matches. Consider using a more specific path or pattern.)\n\n(Some paths were inaccessible and skipped)",
|
value:
|
||||||
|
"Found 1 matches\nsrc/index.ts:\n Line 3: needle preview...\n\n(Results are truncated: showing first 1 matches. Consider using a more specific path or pattern.)\n\n(Some paths were inaccessible and skipped)",
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -226,7 +239,10 @@ describe("GrepTool", () => {
|
|||||||
it.effect("returns a useful tool error for an invalid regex", () =>
|
it.effect("returns a useful tool error for an invalid regex", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
reset()
|
reset()
|
||||||
searchFailure = new Ripgrep.InvalidPatternError({ pattern: "[", message: "regex parse error: unclosed character class" })
|
searchFailure = new Ripgrep.InvalidPatternError({
|
||||||
|
pattern: "[",
|
||||||
|
message: "regex parse error: unclosed character class",
|
||||||
|
})
|
||||||
|
|
||||||
expect(yield* execute({ pattern: "[" })).toEqual({
|
expect(yield* execute({ pattern: "[" })).toEqual({
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -261,7 +277,9 @@ describe("GrepTool", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
value: "Found 1 matches\ndocs:guide.md:\n Line 1: needle docs\n",
|
value: "Found 1 matches\ndocs:guide.md:\n Line 1: needle docs\n",
|
||||||
})
|
})
|
||||||
}).pipe(Effect.provide(provideLive(tmp.path, references({ docs: { name: "docs", kind: "local", path: docs } }))))
|
}).pipe(
|
||||||
|
Effect.provide(provideLive(tmp.path, references({ docs: { name: "docs", kind: "local", path: docs } }))),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,9 +47,10 @@ describe("ToolOutputStore", () => {
|
|||||||
it.live("returns under-limit text unchanged without writing a resource", () =>
|
it.live("returns under-limit text unchanged without writing a resource", () =>
|
||||||
withStore(({ store }) =>
|
withStore(({ store }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(
|
expect(yield* store.truncate({ sessionID, toolCallID: "call-short", content: "line one\nline two" })).toEqual({
|
||||||
yield* store.truncate({ sessionID, toolCallID: "call-short", content: "line one\nline two" }),
|
content: "line one\nline two",
|
||||||
).toEqual({ content: "line one\nline two", truncated: false })
|
truncated: false,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -92,7 +93,12 @@ describe("ToolOutputStore", () => {
|
|||||||
it.live("keeps one-line previews bounded", () =>
|
it.live("keeps one-line previews bounded", () =>
|
||||||
withStore(({ store }) =>
|
withStore(({ store }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const result = yield* store.truncate({ sessionID, toolCallID: "call-one-line", content: "one\ntwo\nthree", maxLines: 1 })
|
const result = yield* store.truncate({
|
||||||
|
sessionID,
|
||||||
|
toolCallID: "call-one-line",
|
||||||
|
content: "one\ntwo\nthree",
|
||||||
|
maxLines: 1,
|
||||||
|
})
|
||||||
|
|
||||||
expect(result.truncated).toBe(true)
|
expect(result.truncated).toBe(true)
|
||||||
if (!result.truncated) throw new Error("expected truncation")
|
if (!result.truncated) throw new Error("expected truncation")
|
||||||
@@ -105,7 +111,12 @@ describe("ToolOutputStore", () => {
|
|||||||
it.live("pages reads within the bounded managed-resource limit", () =>
|
it.live("pages reads within the bounded managed-resource limit", () =>
|
||||||
withStore(({ root, store, fs }) =>
|
withStore(({ root, store, fs }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resource = yield* store.write({ sessionID, toolCallID: "call-page", content: "0123456789", name: "out.txt" })
|
const resource = yield* store.write({
|
||||||
|
sessionID,
|
||||||
|
toolCallID: "call-page",
|
||||||
|
content: "0123456789",
|
||||||
|
name: "out.txt",
|
||||||
|
})
|
||||||
const first = yield* store.read({ sessionID, uri: resource.uri, limit: 4 })
|
const first = yield* store.read({ sessionID, uri: resource.uri, limit: 4 })
|
||||||
const second = yield* store.read({ sessionID, uri: resource.uri, offset: first.next, limit: 4 })
|
const second = yield* store.read({ sessionID, uri: resource.uri, offset: first.next, limit: 4 })
|
||||||
const last = yield* store.read({ sessionID, uri: resource.uri, offset: second.next, limit: 4 })
|
const last = yield* store.read({ sessionID, uri: resource.uri, offset: second.next, limit: 4 })
|
||||||
@@ -114,7 +125,13 @@ describe("ToolOutputStore", () => {
|
|||||||
expect(second).toMatchObject({ content: "4567", offset: 4, truncated: true, next: 8 })
|
expect(second).toMatchObject({ content: "4567", offset: 4, truncated: true, next: 8 })
|
||||||
expect(last).toMatchObject({ content: "89", offset: 8, truncated: false })
|
expect(last).toMatchObject({ content: "89", offset: 8, truncated: false })
|
||||||
expect(last.resource).toEqual({ uri: resource.uri, mime: "text/plain", name: "out.txt", size: 10 })
|
expect(last.resource).toEqual({ uri: resource.uri, mime: "text/plain", name: "out.txt", size: 10 })
|
||||||
expect(JSON.parse(yield* fs.readFileString(path.join(root, "tool-output", "managed", `${resource.uri.slice("tool-output://".length)}.json`)))).toMatchObject({
|
expect(
|
||||||
|
JSON.parse(
|
||||||
|
yield* fs.readFileString(
|
||||||
|
path.join(root, "tool-output", "managed", `${resource.uri.slice("tool-output://".length)}.json`),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
sessionID,
|
sessionID,
|
||||||
toolCallID: "call-page",
|
toolCallID: "call-page",
|
||||||
})
|
})
|
||||||
@@ -165,9 +182,9 @@ describe("ToolOutputStore", () => {
|
|||||||
({ store }) =>
|
({ store }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(yield* store.limits()).toEqual({ maxLines: 2, maxBytes: 1_000 })
|
expect(yield* store.limits()).toEqual({ maxLines: 2, maxBytes: 1_000 })
|
||||||
expect((yield* store.truncate({ sessionID, toolCallID: "call-config", content: "one\ntwo\nthree" })).truncated).toBe(
|
expect(
|
||||||
true,
|
(yield* store.truncate({ sessionID, toolCallID: "call-config", content: "one\ntwo\nthree" })).truncated,
|
||||||
)
|
).toBe(true)
|
||||||
}),
|
}),
|
||||||
new Config.Info({ tool_output: new ConfigToolOutput.Info({ max_lines: 2, max_bytes: 1_000 }) }),
|
new Config.Info({ tool_output: new ConfigToolOutput.Info({ max_lines: 2, max_bytes: 1_000 }) }),
|
||||||
),
|
),
|
||||||
@@ -186,7 +203,10 @@ describe("ToolOutputStore", () => {
|
|||||||
const unrelatedManaged = path.join(directory, "unrelated.txt")
|
const unrelatedManaged = path.join(directory, "unrelated.txt")
|
||||||
const record = JSON.parse(yield* fs.readFileString(oldMetadata))
|
const record = JSON.parse(yield* fs.readFileString(oldMetadata))
|
||||||
|
|
||||||
yield* fs.writeFileString(oldMetadata, JSON.stringify({ ...record, created: Date.now() - 8 * 24 * 60 * 60 * 1_000 }))
|
yield* fs.writeFileString(
|
||||||
|
oldMetadata,
|
||||||
|
JSON.stringify({ ...record, created: Date.now() - 8 * 24 * 60 * 60 * 1_000 }),
|
||||||
|
)
|
||||||
yield* fs.writeFileString(unrelated, "keep")
|
yield* fs.writeFileString(unrelated, "keep")
|
||||||
yield* fs.writeFileString(unrelatedManaged, "keep")
|
yield* fs.writeFileString(unrelatedManaged, "keep")
|
||||||
yield* store.cleanup()
|
yield* store.cleanup()
|
||||||
|
|||||||
@@ -362,7 +362,12 @@ describe("ReadTool", () => {
|
|||||||
expect(
|
expect(
|
||||||
yield* registry.execute({
|
yield* registry.execute({
|
||||||
sessionID,
|
sessionID,
|
||||||
call: { type: "tool-call", id: "call-large", name: "read", input: { path: "large.txt", offset: 2, limit: 1 } },
|
call: {
|
||||||
|
type: "tool-call",
|
||||||
|
id: "call-large",
|
||||||
|
name: "read",
|
||||||
|
input: { path: "large.txt", offset: 2, limit: 1 },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
type: "json",
|
type: "json",
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ describe("SkillTool", () => {
|
|||||||
let bootWaited = false
|
let bootWaited = false
|
||||||
const boot = Layer.succeed(
|
const boot = Layer.succeed(
|
||||||
PluginBoot.Service,
|
PluginBoot.Service,
|
||||||
PluginBoot.Service.of({ wait: () => Effect.sync(() => { bootWaited = true }) }),
|
PluginBoot.Service.of({
|
||||||
|
wait: () =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
bootWaited = true
|
||||||
|
}),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
const permission = Layer.succeed(
|
const permission = Layer.succeed(
|
||||||
PermissionV2.Service,
|
PermissionV2.Service,
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ describe("WebSearchTool provider selection", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("supports an explicit operational override", () => {
|
test("supports an explicit operational override", () => {
|
||||||
expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "parallel")).toBe("parallel")
|
expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "parallel")).toBe(
|
||||||
|
"parallel",
|
||||||
|
)
|
||||||
expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "exa")).toBe("exa")
|
expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "exa")).toBe("exa")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,9 +49,11 @@ describe("WebSearchTool MCP response parser", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("parses SSE JSON-RPC responses and ignores non-JSON frames", async () => {
|
test("parses SSE JSON-RPC responses and ignores non-JSON frames", async () => {
|
||||||
expect(await Effect.runPromise(WebSearchTool.parseResponse(`data: [DONE]\nevent: message\ndata: ${payload("search results")}\n\n`))).toBe(
|
expect(
|
||||||
"search results",
|
await Effect.runPromise(
|
||||||
)
|
WebSearchTool.parseResponse(`data: [DONE]\nevent: message\ndata: ${payload("search results")}\n\n`),
|
||||||
|
),
|
||||||
|
).toBe("search results")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -151,7 +155,13 @@ describe("WebSearchTool contribution", () => {
|
|||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
id: "call-exa",
|
id: "call-exa",
|
||||||
name: "websearch",
|
name: "websearch",
|
||||||
input: { query: "effect typescript", numResults: 3, livecrawl: "preferred", type: "fast", contextMaxCharacters: 2500 },
|
input: {
|
||||||
|
query: "effect typescript",
|
||||||
|
numResults: 3,
|
||||||
|
livecrawl: "preferred",
|
||||||
|
type: "fast",
|
||||||
|
contextMaxCharacters: 2500,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual({ type: "text", value: "exa results" })
|
).toEqual({ type: "text", value: "exa results" })
|
||||||
@@ -161,7 +171,14 @@ describe("WebSearchTool contribution", () => {
|
|||||||
action: "websearch",
|
action: "websearch",
|
||||||
resources: ["effect typescript"],
|
resources: ["effect typescript"],
|
||||||
save: ["*"],
|
save: ["*"],
|
||||||
metadata: { query: "effect typescript", numResults: 3, livecrawl: "preferred", type: "fast", contextMaxCharacters: 2500, provider: "exa" },
|
metadata: {
|
||||||
|
query: "effect typescript",
|
||||||
|
numResults: 3,
|
||||||
|
livecrawl: "preferred",
|
||||||
|
type: "fast",
|
||||||
|
contextMaxCharacters: 2500,
|
||||||
|
provider: "exa",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
expect(requests).toEqual([
|
expect(requests).toEqual([
|
||||||
@@ -174,7 +191,13 @@ describe("WebSearchTool contribution", () => {
|
|||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
params: {
|
params: {
|
||||||
name: "web_search_exa",
|
name: "web_search_exa",
|
||||||
arguments: { query: "effect typescript", type: "fast", numResults: 3, livecrawl: "preferred", contextMaxCharacters: 2500 },
|
arguments: {
|
||||||
|
query: "effect typescript",
|
||||||
|
type: "fast",
|
||||||
|
numResults: 3,
|
||||||
|
livecrawl: "preferred",
|
||||||
|
contextMaxCharacters: 2500,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -211,7 +234,10 @@ describe("WebSearchTool contribution", () => {
|
|||||||
expect(requests[0]?.body).not.toHaveProperty("params.arguments.model_name")
|
expect(requests[0]?.body).not.toHaveProperty("params.arguments.model_name")
|
||||||
expect(settled).toEqual({
|
expect(settled).toEqual({
|
||||||
result: { type: "text", value: "parallel results" },
|
result: { type: "text", value: "parallel results" },
|
||||||
output: { structured: { provider: "parallel", text: "parallel results", truncated: false }, content: [{ type: "text", text: "parallel results" }] },
|
output: {
|
||||||
|
structured: { provider: "parallel", text: "parallel results", truncated: false },
|
||||||
|
content: [{ type: "text", text: "parallel results" }],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(JSON.stringify(settled)).not.toContain("parallel-secret")
|
expect(JSON.stringify(settled)).not.toContain("parallel-secret")
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -114,7 +114,11 @@ const streamWithTools = Effect.gen(function* () {
|
|||||||
// A durable agent would persist these messages before starting another
|
// A durable agent would persist these messages before starting another
|
||||||
// raw model turn. This tutorial keeps the boundary visible instead.
|
// raw model turn. This tutorial keeps the boundary visible instead.
|
||||||
const followUp = LLM.updateRequest(request, {
|
const followUp = LLM.updateRequest(request, {
|
||||||
messages: [...request.messages, Message.assistant([event]), Message.tool({ ...event, result: dispatched.result })],
|
messages: [
|
||||||
|
...request.messages,
|
||||||
|
Message.assistant([event]),
|
||||||
|
Message.tool({ ...event, result: dispatched.result }),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
console.log("follow-up history messages:", followUp.messages.length)
|
console.log("follow-up history messages:", followUp.messages.length)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,11 +363,15 @@ const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSy
|
|||||||
const previous = messages[index - 1]
|
const previous = messages[index - 1]
|
||||||
const next = messages[index + 1]
|
const next = messages[index + 1]
|
||||||
if (!previous)
|
if (!previous)
|
||||||
return yield* invalid("Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system")
|
return yield* invalid(
|
||||||
|
"Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system",
|
||||||
|
)
|
||||||
if (previous.role === "system")
|
if (previous.role === "system")
|
||||||
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
||||||
if (endsInLocalToolUse(previous))
|
if (endsInLocalToolUse(previous))
|
||||||
return yield* invalid("Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result")
|
return yield* invalid(
|
||||||
|
"Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result",
|
||||||
|
)
|
||||||
if (previous.role !== "user" && previous.role !== "tool" && !endsInServerToolUse(previous))
|
if (previous.role !== "user" && previous.role !== "tool" && !endsInServerToolUse(previous))
|
||||||
return yield* invalid(
|
return yield* invalid(
|
||||||
"Anthropic Messages chronological system updates must follow a user message, tool result, or assistant server tool use",
|
"Anthropic Messages chronological system updates must follow a user message, tool result, or assistant server tool use",
|
||||||
@@ -375,7 +379,9 @@ const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSy
|
|||||||
if (next?.role === "system")
|
if (next?.role === "system")
|
||||||
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
|
||||||
if (next && next.role !== "assistant")
|
if (next && next.role !== "assistant")
|
||||||
return yield* invalid("Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message")
|
return yield* invalid(
|
||||||
|
"Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message",
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
|
const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
|
||||||
@@ -409,7 +415,8 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
|||||||
const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
|
const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
|
||||||
const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
|
const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
|
||||||
const previous = messages.at(-1)
|
const previous = messages.at(-1)
|
||||||
if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
|
if (previous?.role === "user")
|
||||||
|
messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
|
||||||
else messages.push({ role: "user", content: [block] })
|
else messages.push({ role: "user", content: [block] })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,7 +243,10 @@ const bedrockMetadata = (metadata: Record<string, unknown>): ProviderMetadata =>
|
|||||||
|
|
||||||
const reasoningSignature = (part: ReasoningPart) => {
|
const reasoningSignature = (part: ReasoningPart) => {
|
||||||
const bedrock = part.providerMetadata?.bedrock
|
const bedrock = part.providerMetadata?.bedrock
|
||||||
return part.encrypted ?? (ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined)
|
return (
|
||||||
|
part.encrypted ??
|
||||||
|
(ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
|
const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
|
||||||
@@ -294,7 +297,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
|||||||
const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message)
|
const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message)
|
||||||
const content = textWithCache(breakpoints, part.text, part.cache)
|
const content = textWithCache(breakpoints, part.text, part.cache)
|
||||||
const previous = messages.at(-1)
|
const previous = messages.at(-1)
|
||||||
if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
|
if (previous?.role === "user")
|
||||||
|
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
|
||||||
else messages.push({ role: "user", content })
|
else messages.push({ role: "user", content })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -532,7 +536,9 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
|||||||
Lifecycle.textEnd(state.lifecycle, events, `text-${index}`),
|
Lifecycle.textEnd(state.lifecycle, events, `text-${index}`),
|
||||||
events,
|
events,
|
||||||
`reasoning-${index}`,
|
`reasoning-${index}`,
|
||||||
state.reasoningSignatures[index] ? bedrockMetadata({ signature: state.reasoningSignatures[index] }) : undefined,
|
state.reasoningSignatures[index]
|
||||||
|
? bedrockMetadata({ signature: state.reasoningSignatures[index] })
|
||||||
|
: undefined,
|
||||||
)
|
)
|
||||||
events.push(...resultEvents)
|
events.push(...resultEvents)
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
|
|||||||
if (message.role === "system") {
|
if (message.role === "system") {
|
||||||
const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message)
|
const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message)
|
||||||
const previous = contents.at(-1)
|
const previous = contents.at(-1)
|
||||||
if (previous?.role === "user") contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] }
|
if (previous?.role === "user")
|
||||||
|
contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] }
|
||||||
else contents.push({ role: "user", parts: [{ text: part.text }] })
|
else contents.push({ role: "user", parts: [{ text: part.text }] })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -405,7 +406,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
|||||||
id,
|
id,
|
||||||
name: part.functionCall.name,
|
name: part.functionCall.name,
|
||||||
input,
|
input,
|
||||||
providerMetadata: part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined,
|
providerMetadata: part.thoughtSignature
|
||||||
|
? googleMetadata({ thoughtSignature: part.thoughtSignature })
|
||||||
|
: undefined,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
hasToolCalls = true
|
hasToolCalls = true
|
||||||
|
|||||||
@@ -226,7 +226,9 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
|||||||
content: content.length === 0 ? null : ProviderShared.joinText(content),
|
content: content.length === 0 ? null : ProviderShared.joinText(content),
|
||||||
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
|
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
|
||||||
reasoning_content:
|
reasoning_content:
|
||||||
reasoning.length > 0 ? reasoning.map((part) => part.text).join("") : openAICompatibleReasoningContent(message.native?.openaiCompatible),
|
reasoning.length > 0
|
||||||
|
? reasoning.map((part) => part.text).join("")
|
||||||
|
: openAICompatibleReasoningContent(message.native?.openaiCompatible),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -343,7 +343,10 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
|||||||
const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message)
|
const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message)
|
||||||
const previous = input.at(-1)
|
const previous = input.at(-1)
|
||||||
if (previous && "role" in previous && previous.role === "user")
|
if (previous && "role" in previous && previous.role === "user")
|
||||||
input[input.length - 1] = { role: "user", content: [...previous.content, { type: "input_text", text: part.text }] }
|
input[input.length - 1] = {
|
||||||
|
role: "user",
|
||||||
|
content: [...previous.content, { type: "input_text", text: part.text }],
|
||||||
|
}
|
||||||
else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
|
else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -397,7 +400,8 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
|
|||||||
if (part.type === "tool-result" && part.providerExecuted === true) {
|
if (part.type === "tool-result" && part.providerExecuted === true) {
|
||||||
flushText()
|
flushText()
|
||||||
const itemID = hostedToolItemID(part)
|
const itemID = hostedToolItemID(part)
|
||||||
if (store !== false && itemID && !hostedToolReferences.has(itemID)) input.push({ type: "item_reference", id: itemID })
|
if (store !== false && itemID && !hostedToolReferences.has(itemID))
|
||||||
|
input.push({ type: "item_reference", id: itemID })
|
||||||
if (itemID) hostedToolReferences.add(itemID)
|
if (itemID) hostedToolReferences.add(itemID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ export const parseJson = (route: string, input: string, message: string) =>
|
|||||||
*/
|
*/
|
||||||
export const joinText = (parts: ReadonlyArray<{ readonly text: string }>) => parts.map((part) => part.text).join("\n")
|
export const joinText = (parts: ReadonlyArray<{ readonly text: string }>) => parts.map((part) => part.text).join("\n")
|
||||||
|
|
||||||
const escapeSystemUpdateText = (text: string) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
const escapeSystemUpdateText = (text: string) =>
|
||||||
|
text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stable fallback representation for chronological `Message.system(...)`
|
* Stable fallback representation for chronological `Message.system(...)`
|
||||||
|
|||||||
@@ -193,7 +193,8 @@ export const ToolOutput = Object.assign(
|
|||||||
type: "content",
|
type: "content",
|
||||||
value: output.content.map((item) => {
|
value: output.content.map((item) => {
|
||||||
if (item.type === "text") return { type: "text", text: item.text }
|
if (item.type === "text") return { type: "text", text: item.text }
|
||||||
if (item.source.type !== "data") throw new Error("Unmaterialized tool file source reached provider conversion")
|
if (item.source.type !== "data")
|
||||||
|
throw new Error("Unmaterialized tool file source reached provider conversion")
|
||||||
return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name }
|
return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name }
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { LLMEvent, type ToolCallPart, ToolFailure, ToolOutput, ToolResultValue, type ToolOutput as ToolOutputType, type ToolResultValue as ToolResultValueType } from "./schema"
|
import {
|
||||||
|
LLMEvent,
|
||||||
|
type ToolCallPart,
|
||||||
|
ToolFailure,
|
||||||
|
ToolOutput,
|
||||||
|
ToolResultValue,
|
||||||
|
type ToolOutput as ToolOutputType,
|
||||||
|
type ToolResultValue as ToolResultValueType,
|
||||||
|
} from "./schema"
|
||||||
import { type AnyTool, type Tools } from "./tool"
|
import { type AnyTool, type Tools } from "./tool"
|
||||||
|
|
||||||
export interface ToolSettlement {
|
export interface ToolSettlement {
|
||||||
@@ -52,11 +60,7 @@ const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect<Tool
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const result = (
|
const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement, error?: unknown): DispatchResult => {
|
||||||
call: ToolCallPart,
|
|
||||||
value: ToolResultValueType | ToolSettlement,
|
|
||||||
error?: unknown,
|
|
||||||
): DispatchResult => {
|
|
||||||
const settlement = ToolResultValue.is(value) ? { result: value } : value
|
const settlement = ToolResultValue.is(value) ? { result: value } : value
|
||||||
return {
|
return {
|
||||||
result: settlement.result,
|
result: settlement.result,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { Effect, JsonSchema, Schema } from "effect"
|
import { Effect, JsonSchema, Schema } from "effect"
|
||||||
import type { ToolCallPart, ToolContent, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
|
import type {
|
||||||
|
ToolCallPart,
|
||||||
|
ToolContent,
|
||||||
|
ToolDefinition as ToolDefinitionClass,
|
||||||
|
ToolOutput as ToolOutputType,
|
||||||
|
} from "./schema"
|
||||||
import { ToolDefinition, ToolFailure, ToolOutput, toolText } from "./schema"
|
import { ToolDefinition, ToolFailure, ToolOutput, toolText } from "./schema"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +56,11 @@ export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSc
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError>
|
readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError>
|
||||||
/** @internal */
|
/** @internal */
|
||||||
readonly _project: (parameters: Schema.Schema.Type<Parameters>, callID: ToolCallPart["id"], output: unknown) => ToolOutputType
|
readonly _project: (
|
||||||
|
parameters: Schema.Schema.Type<Parameters>,
|
||||||
|
callID: ToolCallPart["id"],
|
||||||
|
output: unknown,
|
||||||
|
) => ToolOutputType
|
||||||
/** @internal */
|
/** @internal */
|
||||||
readonly _legacyResult: boolean
|
readonly _legacyResult: boolean
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
|||||||
@@ -117,7 +117,11 @@ const appendText = (
|
|||||||
) => {
|
) => {
|
||||||
const last = content.at(-1)
|
const last = content.at(-1)
|
||||||
if (last?.type === type) {
|
if (last?.type === type) {
|
||||||
content[content.length - 1] = { ...last, text: `${last.text}${text}`, providerMetadata: providerMetadata ?? last.providerMetadata }
|
content[content.length - 1] = {
|
||||||
|
...last,
|
||||||
|
text: `${last.text}${text}`,
|
||||||
|
providerMetadata: providerMetadata ?? last.providerMetadata,
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content.push({ type, text, providerMetadata })
|
content.push({ type, text, providerMetadata })
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ describe("llm constructors", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("builds chronological text-only system updates separately from the initial system prompt", () => {
|
test("builds chronological text-only system updates separately from the initial system prompt", () => {
|
||||||
const update = Message.system([{ type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) }])
|
const update = Message.system([
|
||||||
|
{ type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) },
|
||||||
|
])
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||||
system: "Initial operator prompt.",
|
system: "Initial operator prompt.",
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ describe("Anthropic Messages route", () => {
|
|||||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model,
|
model,
|
||||||
messages: [Message.user("Before."), Message.system("Treat </system-update> literally."), Message.assistant("After.")],
|
messages: [
|
||||||
|
Message.user("Before."),
|
||||||
|
Message.system("Treat </system-update> literally."),
|
||||||
|
Message.assistant("After."),
|
||||||
|
],
|
||||||
cache: "none",
|
cache: "none",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -127,19 +131,19 @@ describe("Anthropic Messages route", () => {
|
|||||||
LLMClient.prepare(LLM.request({ model: opus48, messages, cache: "none" })).pipe(Effect.flip)
|
LLMClient.prepare(LLM.request({ model: opus48, messages, cache: "none" })).pipe(Effect.flip)
|
||||||
|
|
||||||
expect((yield* placementError([Message.system("First.")])).message).toContain("cannot be the first message")
|
expect((yield* placementError([Message.system("First.")])).message).toContain("cannot be the first message")
|
||||||
expect((yield* placementError([Message.user("Before."), Message.system("One."), Message.system("Two.")])).message)
|
|
||||||
.toContain("cannot be consecutive")
|
|
||||||
expect((yield* placementError([Message.assistant("Plain."), Message.system("After plain assistant.")])).message)
|
|
||||||
.toContain("must follow a user message, tool result, or assistant server tool use")
|
|
||||||
expect(
|
expect(
|
||||||
(
|
(yield* placementError([Message.user("Before."), Message.system("One."), Message.system("Two.")])).message,
|
||||||
yield* placementError([
|
).toContain("cannot be consecutive")
|
||||||
|
expect(
|
||||||
|
(yield* placementError([Message.assistant("Plain."), Message.system("After plain assistant.")])).message,
|
||||||
|
).toContain("must follow a user message, tool result, or assistant server tool use")
|
||||||
|
expect(
|
||||||
|
(yield* placementError([
|
||||||
Message.user("Use the tool."),
|
Message.user("Use the tool."),
|
||||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
|
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
|
||||||
Message.system("Too early."),
|
Message.system("Too early."),
|
||||||
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
|
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
|
||||||
])
|
])).message,
|
||||||
).message,
|
|
||||||
).toContain("cannot appear between a local tool call and its tool result")
|
).toContain("cannot appear between a local tool call and its tool result")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -327,7 +327,10 @@ describe("Bedrock Converse route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
expect(prepared.body.messages).toEqual([
|
expect(prepared.body.messages).toEqual([
|
||||||
{ role: "assistant", content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }] },
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }],
|
||||||
|
},
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -55,12 +55,19 @@ describe("OpenAI Chat route", () => {
|
|||||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model,
|
model,
|
||||||
messages: [Message.user("Before."), Message.system("Treat <admin> & data literally."), Message.assistant("After.")],
|
messages: [
|
||||||
|
Message.user("Before."),
|
||||||
|
Message.system("Treat <admin> & data literally."),
|
||||||
|
Message.assistant("After."),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prepared.body.messages).toEqual([
|
expect(prepared.body.messages).toEqual([
|
||||||
{ role: "user", content: "Before.\n<system-update>\nTreat <admin> & data literally.\n</system-update>" },
|
{
|
||||||
|
role: "user",
|
||||||
|
content: "Before.\n<system-update>\nTreat <admin> & data literally.\n</system-update>",
|
||||||
|
},
|
||||||
{ role: "assistant", content: "After." },
|
{ role: "assistant", content: "After." },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
@@ -71,7 +78,12 @@ describe("OpenAI Chat route", () => {
|
|||||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model,
|
model,
|
||||||
messages: [Message.assistant([{ type: "reasoning", text: "thinking" }, { type: "text", text: "Hello" }])],
|
messages: [
|
||||||
|
Message.assistant([
|
||||||
|
{ type: "reasoning", text: "thinking" },
|
||||||
|
{ type: "text", text: "Hello" },
|
||||||
|
]),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ describe("OpenAI Responses route", () => {
|
|||||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model,
|
model,
|
||||||
messages: [Message.user("Before."), Message.system("Treat </system-update> literally."), Message.assistant("After.")],
|
messages: [
|
||||||
|
Message.user("Before."),
|
||||||
|
Message.system("Treat </system-update> literally."),
|
||||||
|
Message.assistant("After."),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Schema, Stream } from "effect"
|
import { Effect, Schema, Stream } from "effect"
|
||||||
import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice, ToolContent, ToolOutput, toolFileSourceFromUri, toDefinitions } from "../src"
|
import {
|
||||||
|
GenerationOptions,
|
||||||
|
LLM,
|
||||||
|
LLMEvent,
|
||||||
|
LLMRequest,
|
||||||
|
LLMResponse,
|
||||||
|
ToolChoice,
|
||||||
|
ToolContent,
|
||||||
|
ToolOutput,
|
||||||
|
toolFileSourceFromUri,
|
||||||
|
toDefinitions,
|
||||||
|
} from "../src"
|
||||||
import { Auth, LLMClient } from "../src/route"
|
import { Auth, LLMClient } from "../src/route"
|
||||||
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
|
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
|
||||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||||
@@ -188,10 +199,12 @@ describe("LLMClient tools", () => {
|
|||||||
execute: () => Effect.succeed({ ok: true }),
|
execute: () => Effect.succeed({ ok: true }),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((yield* ToolRuntime.dispatch({ text }, LLMEvent.toolCall({ id: "call_text", name: "text", input: {} }))).output)
|
expect(
|
||||||
.toEqual({ structured: "hello", content: [{ type: "text", text: "hello" }] })
|
(yield* ToolRuntime.dispatch({ text }, LLMEvent.toolCall({ id: "call_text", name: "text", input: {} }))).output,
|
||||||
expect((yield* ToolRuntime.dispatch({ json }, LLMEvent.toolCall({ id: "call_json", name: "json", input: {} }))).output)
|
).toEqual({ structured: "hello", content: [{ type: "text", text: "hello" }] })
|
||||||
.toEqual({ structured: { ok: true }, content: [] })
|
expect(
|
||||||
|
(yield* ToolRuntime.dispatch({ json }, LLMEvent.toolCall({ id: "call_json", name: "json", input: {} }))).output,
|
||||||
|
).toEqual({ structured: { ok: true }, content: [] })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -204,12 +217,16 @@ describe("LLMClient tools", () => {
|
|||||||
source: { type: "data", data: "AAAA" },
|
source: { type: "data", data: "AAAA" },
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
})
|
})
|
||||||
expect(decode({ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" })).toEqual({
|
expect(
|
||||||
|
decode({ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }),
|
||||||
|
).toEqual({
|
||||||
type: "file",
|
type: "file",
|
||||||
source: { type: "url", url: "https://example.test/image.png" },
|
source: { type: "url", url: "https://example.test/image.png" },
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
})
|
})
|
||||||
expect(decode({ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" })).toEqual({
|
expect(
|
||||||
|
decode({ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" }),
|
||||||
|
).toEqual({
|
||||||
type: "file",
|
type: "file",
|
||||||
source: { type: "file", uri: "file:///tmp/image.png" },
|
source: { type: "file", uri: "file:///tmp/image.png" },
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
@@ -226,16 +243,29 @@ describe("LLMClient tools", () => {
|
|||||||
).toEqual({ type: "content", value: [{ type: "media", mediaType: "image/png", data: "AAAA" }] })
|
).toEqual({ type: "content", value: [{ type: "media", mediaType: "image/png", data: "AAAA" }] })
|
||||||
expect(
|
expect(
|
||||||
ToolOutput.toResultValue(
|
ToolOutput.toResultValue(
|
||||||
ToolOutput.make({}, [{ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }]),
|
ToolOutput.make({}, [
|
||||||
|
{ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" },
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
).toEqual({ type: "error", value: 'Tool file source "url" must be materialized to inline data before provider conversion' })
|
).toEqual({
|
||||||
|
type: "error",
|
||||||
|
value: 'Tool file source "url" must be materialized to inline data before provider conversion',
|
||||||
|
})
|
||||||
expect(
|
expect(
|
||||||
ToolOutput.toResultValue(
|
ToolOutput.toResultValue(
|
||||||
ToolOutput.make({}, [{ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" }]),
|
ToolOutput.make({}, [
|
||||||
|
{ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" },
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
).toEqual({ type: "error", value: 'Tool file source "file" must be materialized to inline data before provider conversion' })
|
).toEqual({
|
||||||
|
type: "error",
|
||||||
|
value: 'Tool file source "file" must be materialized to inline data before provider conversion',
|
||||||
|
})
|
||||||
expect(toolFileSourceFromUri("data:image/png;base64,AAAA")).toEqual({ type: "data", data: "AAAA" })
|
expect(toolFileSourceFromUri("data:image/png;base64,AAAA")).toEqual({ type: "data", data: "AAAA" })
|
||||||
expect(toolFileSourceFromUri("https://example.test/image.png")).toEqual({ type: "url", url: "https://example.test/image.png" })
|
expect(toolFileSourceFromUri("https://example.test/image.png")).toEqual({
|
||||||
|
type: "url",
|
||||||
|
url: "https://example.test/image.png",
|
||||||
|
})
|
||||||
expect(toolFileSourceFromUri("file:///tmp/image.png")).toEqual({ type: "file", uri: "file:///tmp/image.png" })
|
expect(toolFileSourceFromUri("file:///tmp/image.png")).toEqual({ type: "file", uri: "file:///tmp/image.png" })
|
||||||
expect(() => toolFileSourceFromUri("opaque-value")).toThrow("Unsupported tool file URI")
|
expect(() => toolFileSourceFromUri("opaque-value")).toThrow("Unsupported tool file URI")
|
||||||
expect(() =>
|
expect(() =>
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ Tool.make({
|
|||||||
parameters: Schema.Struct({ city: Schema.String }),
|
parameters: Schema.Struct({ city: Schema.String }),
|
||||||
success: Schema.Struct({ forecast: Schema.NumberFromString }),
|
success: Schema.Struct({ forecast: Schema.NumberFromString }),
|
||||||
execute: () => Effect.succeed({ forecast: 1 }),
|
execute: () => Effect.succeed({ forecast: 1 }),
|
||||||
toModelOutput: ({ callID, parameters, output }) => [{ type: "text", text: `${callID}:${parameters.city}:${output.forecast}` }],
|
toModelOutput: ({ callID, parameters, output }) => [
|
||||||
|
{ type: "text", text: `${callID}:${parameters.city}:${output.forecast}` },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
LLM.stream(request)
|
LLM.stream(request)
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ function latestTool(assistant: SessionMessageAssistant | undefined, callID?: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
function latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
|
function latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
|
||||||
return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID)
|
return assistant?.content.findLast(
|
||||||
|
(item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
|
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
|
||||||
@@ -208,19 +210,28 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
|||||||
break
|
break
|
||||||
case "session.next.tool.input.delta":
|
case "session.next.tool.input.delta":
|
||||||
update(event.properties.sessionID, (draft) => {
|
update(event.properties.sessionID, (draft) => {
|
||||||
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
|
const match = latestTool(
|
||||||
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||||
|
event.properties.callID,
|
||||||
|
)
|
||||||
if (match?.state.status === "pending") match.state.input += event.properties.delta
|
if (match?.state.status === "pending") match.state.input += event.properties.delta
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "session.next.tool.input.ended":
|
case "session.next.tool.input.ended":
|
||||||
update(event.properties.sessionID, (draft) => {
|
update(event.properties.sessionID, (draft) => {
|
||||||
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
|
const match = latestTool(
|
||||||
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||||
|
event.properties.callID,
|
||||||
|
)
|
||||||
if (match?.state.status === "pending") match.state.input = event.properties.text
|
if (match?.state.status === "pending") match.state.input = event.properties.text
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "session.next.tool.called":
|
case "session.next.tool.called":
|
||||||
update(event.properties.sessionID, (draft) => {
|
update(event.properties.sessionID, (draft) => {
|
||||||
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
|
const match = latestTool(
|
||||||
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||||
|
event.properties.callID,
|
||||||
|
)
|
||||||
if (!match) return
|
if (!match) return
|
||||||
match.time.ran = event.properties.timestamp
|
match.time.ran = event.properties.timestamp
|
||||||
match.provider = event.properties.provider
|
match.provider = event.properties.provider
|
||||||
@@ -229,7 +240,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
|||||||
break
|
break
|
||||||
case "session.next.tool.progress":
|
case "session.next.tool.progress":
|
||||||
update(event.properties.sessionID, (draft) => {
|
update(event.properties.sessionID, (draft) => {
|
||||||
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
|
const match = latestTool(
|
||||||
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||||
|
event.properties.callID,
|
||||||
|
)
|
||||||
if (match?.state.status !== "running") return
|
if (match?.state.status !== "running") return
|
||||||
match.state.structured = event.properties.structured
|
match.state.structured = event.properties.structured
|
||||||
match.state.content = [...event.properties.content]
|
match.state.content = [...event.properties.content]
|
||||||
@@ -237,7 +251,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
|||||||
break
|
break
|
||||||
case "session.next.tool.success":
|
case "session.next.tool.success":
|
||||||
update(event.properties.sessionID, (draft) => {
|
update(event.properties.sessionID, (draft) => {
|
||||||
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
|
const match = latestTool(
|
||||||
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||||
|
event.properties.callID,
|
||||||
|
)
|
||||||
if (match?.state.status !== "running") return
|
if (match?.state.status !== "running") return
|
||||||
match.state = {
|
match.state = {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
@@ -252,7 +269,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
|||||||
break
|
break
|
||||||
case "session.next.tool.failed":
|
case "session.next.tool.failed":
|
||||||
update(event.properties.sessionID, (draft) => {
|
update(event.properties.sessionID, (draft) => {
|
||||||
const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
|
const match = latestTool(
|
||||||
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||||
|
event.properties.callID,
|
||||||
|
)
|
||||||
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
||||||
match.state = {
|
match.state = {
|
||||||
status: "error",
|
status: "error",
|
||||||
@@ -287,7 +307,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
|||||||
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
||||||
if (match) {
|
if (match) {
|
||||||
match.text = event.properties.text
|
match.text = event.properties.text
|
||||||
if (event.properties.providerMetadata !== undefined) match.providerMetadata = event.properties.providerMetadata
|
if (event.properties.providerMetadata !== undefined)
|
||||||
|
match.providerMetadata = event.properties.providerMetadata
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1087,7 +1087,8 @@ function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
|
|||||||
return (content ?? [])
|
return (content ?? [])
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (item.type === "text") return item.text.trim()
|
if (item.type === "text") return item.text.trim()
|
||||||
const source = item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri
|
const source =
|
||||||
|
item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri
|
||||||
return `[file ${item.name ?? source}]`
|
return `[file ${item.name ?? source}]`
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import { asSchema, type ModelMessage, type Tool } from "ai"
|
|||||||
import { Cause, Effect, FiberSet, Queue } from "effect"
|
import { Cause, Effect, FiberSet, Queue } from "effect"
|
||||||
import * as Stream from "effect/Stream"
|
import * as Stream from "effect/Stream"
|
||||||
import { FetchHttpClient } from "effect/unstable/http"
|
import { FetchHttpClient } from "effect/unstable/http"
|
||||||
import { LLMRequest, Tool as NativeTool, ToolFailure, ToolRuntime, toDefinitions, type JsonSchema, type LLMEvent } from "@opencode-ai/llm"
|
import {
|
||||||
|
LLMRequest,
|
||||||
|
Tool as NativeTool,
|
||||||
|
ToolFailure,
|
||||||
|
ToolRuntime,
|
||||||
|
toDefinitions,
|
||||||
|
type JsonSchema,
|
||||||
|
type LLMEvent,
|
||||||
|
} from "@opencode-ai/llm"
|
||||||
import type { LLMClientShape } from "@opencode-ai/llm/route"
|
import type { LLMClientShape } from "@opencode-ai/llm/route"
|
||||||
import { LLMNative } from "./native-request"
|
import { LLMNative } from "./native-request"
|
||||||
|
|
||||||
|
|||||||
@@ -426,7 +426,9 @@ export const layer = Layer.effect(
|
|||||||
case "tool-input-delta":
|
case "tool-input-delta":
|
||||||
{
|
{
|
||||||
const toolCall = yield* ensureToolCall(value)
|
const toolCall = yield* ensureToolCall(value)
|
||||||
const assistantMessageID = flags.experimentalEventSystem ? yield* requireV2AssistantMessage(toolCall.call) : undefined
|
const assistantMessageID = flags.experimentalEventSystem
|
||||||
|
? yield* requireV2AssistantMessage(toolCall.call)
|
||||||
|
: undefined
|
||||||
if (assistantMessageID) {
|
if (assistantMessageID) {
|
||||||
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
@@ -589,12 +591,19 @@ export const layer = Layer.effect(
|
|||||||
const content = [
|
const content = [
|
||||||
ToolOutput.text({ type: "text", text: output.output }),
|
ToolOutput.text({ type: "text", text: output.output }),
|
||||||
...(output.attachments?.map((item: SessionV1.FilePart) =>
|
...(output.attachments?.map((item: SessionV1.FilePart) =>
|
||||||
ToolOutput.file({ type: "file", source: toolFileSourceFromUri(item.url), mime: item.mime, name: item.filename }),
|
ToolOutput.file({
|
||||||
|
type: "file",
|
||||||
|
source: toolFileSourceFromUri(item.url),
|
||||||
|
mime: item.mime,
|
||||||
|
name: item.filename,
|
||||||
|
}),
|
||||||
) ?? []),
|
) ?? []),
|
||||||
]
|
]
|
||||||
const unsupported = content.find((item) => item.type === "file" && item.source.type !== "data")
|
const unsupported = content.find((item) => item.type === "file" && item.source.type !== "data")
|
||||||
if (unsupported?.type === "file") {
|
if (unsupported?.type === "file") {
|
||||||
const error = new Error(`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`)
|
const error = new Error(
|
||||||
|
`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`,
|
||||||
|
)
|
||||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
assistantMessageID,
|
assistantMessageID,
|
||||||
@@ -611,7 +620,8 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
yield* failToolCall(value.id, error)
|
yield* failToolCall(value.id, error)
|
||||||
return
|
return
|
||||||
} else yield* events.publish(SessionEvent.Tool.Success, {
|
} else
|
||||||
|
yield* events.publish(SessionEvent.Tool.Success, {
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
assistantMessageID,
|
assistantMessageID,
|
||||||
callID: value.id,
|
callID: value.id,
|
||||||
|
|||||||
@@ -107,7 +107,11 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
|
|||||||
|
|
||||||
await wait(() => {
|
await wait(() => {
|
||||||
const assistant = sync.session.message.fromSession("session-1")[0]
|
const assistant = sync.session.message.fromSession("session-1")[0]
|
||||||
return assistant?.type === "assistant" && assistant.content[0]?.type === "tool" && assistant.content[0].state.status === "error"
|
return (
|
||||||
|
assistant?.type === "assistant" &&
|
||||||
|
assistant.content[0]?.type === "tool" &&
|
||||||
|
assistant.content[0].state.status === "error"
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const assistant = sync.session.message.fromSession("session-1")[0]
|
const assistant = sync.session.message.fromSession("session-1")[0]
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const appCache: Partial<Record<string, CachedApp>> = {}
|
|||||||
export async function disposeApps() {
|
export async function disposeApps() {
|
||||||
const apps = Object.values(appCache)
|
const apps = Object.values(appCache)
|
||||||
for (const key of Object.keys(appCache)) delete appCache[key]
|
for (const key of Object.keys(appCache)) delete appCache[key]
|
||||||
await Promise.all(apps.flatMap((app) => app === undefined ? [] : [app.dispose()]))
|
await Promise.all(apps.flatMap((app) => (app === undefined ? [] : [app.dispose()])))
|
||||||
}
|
}
|
||||||
|
|
||||||
function app(modules: Runtime, options: CallOptions) {
|
function app(modules: Runtime, options: CallOptions) {
|
||||||
|
|||||||
@@ -493,10 +493,12 @@ describe("session HttpApi", () => {
|
|||||||
direction: "next",
|
direction: "next",
|
||||||
})
|
})
|
||||||
|
|
||||||
const nextMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${messageCursor}`, { headers })
|
const nextMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${messageCursor}`, {
|
||||||
expect((yield* json<{ items: SessionMessage.Message[] }>(nextMessagePage)).items.map((message) => message.id)).toEqual([
|
headers,
|
||||||
firstMessage.id,
|
})
|
||||||
])
|
expect(
|
||||||
|
(yield* json<{ items: SessionMessage.Message[] }>(nextMessagePage)).items.map((message) => message.id),
|
||||||
|
).toEqual([firstMessage.id])
|
||||||
|
|
||||||
const legacyMessageCursor = Buffer.from(
|
const legacyMessageCursor = Buffer.from(
|
||||||
JSON.stringify({ id: secondMessage.id, time: 1, order: "desc", direction: "next" }),
|
JSON.stringify({ id: secondMessage.id, time: 1, order: "desc", direction: "next" }),
|
||||||
@@ -504,9 +506,9 @@ describe("session HttpApi", () => {
|
|||||||
const legacyMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${legacyMessageCursor}`, {
|
const legacyMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${legacyMessageCursor}`, {
|
||||||
headers,
|
headers,
|
||||||
})
|
})
|
||||||
expect((yield* json<{ items: SessionMessage.Message[] }>(legacyMessagePage)).items.map((message) => message.id)).toEqual([
|
expect(
|
||||||
firstMessage.id,
|
(yield* json<{ items: SessionMessage.Message[] }>(legacyMessagePage)).items.map((message) => message.id),
|
||||||
])
|
).toEqual([firstMessage.id])
|
||||||
|
|
||||||
const messageCursorWithOrder = yield* request(
|
const messageCursorWithOrder = yield* request(
|
||||||
`/api/session/${session.id}/message?cursor=${messageCursor}&order=asc`,
|
`/api/session/${session.id}/message?cursor=${messageCursor}&order=asc`,
|
||||||
|
|||||||
@@ -291,9 +291,7 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
|
|||||||
redactor: recordingRedactor,
|
redactor: recordingRedactor,
|
||||||
})
|
})
|
||||||
const recordedClient = LLMClient.layer.pipe(
|
const recordedClient = LLMClient.layer.pipe(
|
||||||
Layer.provide(
|
Layer.provide(Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer)),
|
||||||
Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return Layer.mergeAll(
|
return Layer.mergeAll(
|
||||||
|
|||||||
@@ -1004,7 +1004,8 @@ itProviderError.live("session.processor effect tests fail provider-executed erro
|
|||||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||||
const settlements: Array<typeof SessionEvent.Tool.Failed.Type> = []
|
const settlements: Array<typeof SessionEvent.Tool.Failed.Type> = []
|
||||||
const off = yield* events.listen((event) => {
|
const off = yield* events.listen((event) => {
|
||||||
if (event.type === SessionEvent.Tool.Failed.type) settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
|
if (event.type === SessionEvent.Tool.Failed.type)
|
||||||
|
settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
|
||||||
return Effect.void
|
return Effect.void
|
||||||
})
|
})
|
||||||
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
|
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
|
||||||
@@ -1059,7 +1060,8 @@ itFragmentFailure.live("session.processor effect tests flush partial v2 fragment
|
|||||||
let reasoning: string | undefined
|
let reasoning: string | undefined
|
||||||
const off = yield* events.listen((event) => {
|
const off = yield* events.listen((event) => {
|
||||||
seen.push(event.type)
|
seen.push(event.type)
|
||||||
if (event.type === SessionEvent.Text.Ended.type) text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
|
if (event.type === SessionEvent.Text.Ended.type)
|
||||||
|
text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
|
||||||
if (event.type === SessionEvent.Reasoning.Ended.type)
|
if (event.type === SessionEvent.Reasoning.Ended.type)
|
||||||
reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text
|
reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text
|
||||||
return Effect.void
|
return Effect.void
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ export type Event =
|
|||||||
| EventSessionError
|
| EventSessionError
|
||||||
| EventInstallationUpdated
|
| EventInstallationUpdated
|
||||||
| EventInstallationUpdateAvailable
|
| EventInstallationUpdateAvailable
|
||||||
| EventPermissionV2Asked
|
|
||||||
| EventPermissionV2Replied
|
|
||||||
| EventFileEdited
|
| EventFileEdited
|
||||||
| EventAccountAdded
|
| EventAccountAdded
|
||||||
| EventAccountRemoved
|
| EventAccountRemoved
|
||||||
| EventAccountSwitched
|
| EventAccountSwitched
|
||||||
|
| EventPermissionV2Asked
|
||||||
|
| EventPermissionV2Replied
|
||||||
| EventFileWatcherUpdated
|
| EventFileWatcherUpdated
|
||||||
| EventPtyCreated
|
| EventPtyCreated
|
||||||
| EventPtyUpdated
|
| EventPtyUpdated
|
||||||
@@ -1163,30 +1163,6 @@ export type GlobalEvent = {
|
|||||||
version: string
|
version: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
id: string
|
|
||||||
type: "permission.v2.asked"
|
|
||||||
properties: {
|
|
||||||
id: string
|
|
||||||
sessionID: string
|
|
||||||
action: string
|
|
||||||
resources: Array<string>
|
|
||||||
save?: Array<string>
|
|
||||||
metadata?: {
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
source?: PermissionV2Source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: string
|
|
||||||
type: "permission.v2.replied"
|
|
||||||
properties: {
|
|
||||||
sessionID: string
|
|
||||||
requestID: string
|
|
||||||
reply: PermissionV2Reply
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
id: string
|
id: string
|
||||||
type: "file.edited"
|
type: "file.edited"
|
||||||
@@ -1217,6 +1193,30 @@ export type GlobalEvent = {
|
|||||||
to?: string
|
to?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
id: string
|
||||||
|
type: "permission.v2.asked"
|
||||||
|
properties: {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
action: string
|
||||||
|
resources: Array<string>
|
||||||
|
save?: Array<string>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
source?: PermissionV2Source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string
|
||||||
|
type: "permission.v2.replied"
|
||||||
|
properties: {
|
||||||
|
sessionID: string
|
||||||
|
requestID: string
|
||||||
|
reply: PermissionV2Reply
|
||||||
|
}
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
id: string
|
id: string
|
||||||
type: "file.watcher.updated"
|
type: "file.watcher.updated"
|
||||||
@@ -2893,14 +2893,6 @@ export type SessionNextRetryError = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PermissionV2Source = {
|
|
||||||
type: "tool"
|
|
||||||
messageID: string
|
|
||||||
callID: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PermissionV2Reply = "once" | "always" | "reject"
|
|
||||||
|
|
||||||
export type AuthOAuthCredential = {
|
export type AuthOAuthCredential = {
|
||||||
type: "oauth"
|
type: "oauth"
|
||||||
refresh: string
|
refresh: string
|
||||||
@@ -2925,6 +2917,14 @@ export type AuthInfo = {
|
|||||||
credential: AuthCredential
|
credential: AuthCredential
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PermissionV2Source = {
|
||||||
|
type: "tool"
|
||||||
|
messageID: string
|
||||||
|
callID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PermissionV2Reply = "once" | "always" | "reject"
|
||||||
|
|
||||||
export type QuestionV2Option = {
|
export type QuestionV2Option = {
|
||||||
/**
|
/**
|
||||||
* Display text (1-5 words, concise)
|
* Display text (1-5 words, concise)
|
||||||
@@ -4404,32 +4404,6 @@ export type EventInstallationUpdateAvailable = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventPermissionV2Asked = {
|
|
||||||
id: string
|
|
||||||
type: "permission.v2.asked"
|
|
||||||
properties: {
|
|
||||||
id: string
|
|
||||||
sessionID: string
|
|
||||||
action: string
|
|
||||||
resources: Array<string>
|
|
||||||
save?: Array<string>
|
|
||||||
metadata?: {
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
source?: PermissionV2Source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventPermissionV2Replied = {
|
|
||||||
id: string
|
|
||||||
type: "permission.v2.replied"
|
|
||||||
properties: {
|
|
||||||
sessionID: string
|
|
||||||
requestID: string
|
|
||||||
reply: PermissionV2Reply
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventFileEdited = {
|
export type EventFileEdited = {
|
||||||
id: string
|
id: string
|
||||||
type: "file.edited"
|
type: "file.edited"
|
||||||
@@ -4464,6 +4438,32 @@ export type EventAccountSwitched = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventPermissionV2Asked = {
|
||||||
|
id: string
|
||||||
|
type: "permission.v2.asked"
|
||||||
|
properties: {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
action: string
|
||||||
|
resources: Array<string>
|
||||||
|
save?: Array<string>
|
||||||
|
metadata?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
source?: PermissionV2Source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventPermissionV2Replied = {
|
||||||
|
id: string
|
||||||
|
type: "permission.v2.replied"
|
||||||
|
properties: {
|
||||||
|
sessionID: string
|
||||||
|
requestID: string
|
||||||
|
reply: PermissionV2Reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type EventFileWatcherUpdated = {
|
export type EventFileWatcherUpdated = {
|
||||||
id: string
|
id: string
|
||||||
type: "file.watcher.updated"
|
type: "file.watcher.updated"
|
||||||
|
|||||||
+191
-191
@@ -11655,12 +11655,6 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/EventInstallationUpdate-available"
|
"$ref": "#/components/schemas/EventInstallationUpdate-available"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/EventPermissionV2Asked"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/EventPermissionV2Replied"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/EventFileEdited"
|
"$ref": "#/components/schemas/EventFileEdited"
|
||||||
},
|
},
|
||||||
@@ -11673,6 +11667,12 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/EventAccountSwitched"
|
"$ref": "#/components/schemas/EventAccountSwitched"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/EventPermissionV2Asked"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/EventPermissionV2Replied"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/schemas/EventFileWatcherUpdated"
|
"$ref": "#/components/schemas/EventFileWatcherUpdated"
|
||||||
},
|
},
|
||||||
@@ -15112,88 +15112,6 @@
|
|||||||
"required": ["id", "type", "properties"],
|
"required": ["id", "type", "properties"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["permission.v2.asked"]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^per"
|
|
||||||
},
|
|
||||||
"sessionID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^ses"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"save": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"$ref": "#/components/schemas/PermissionV2Source"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "sessionID", "action", "resources"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "type", "properties"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["permission.v2.replied"]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"sessionID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^ses"
|
|
||||||
},
|
|
||||||
"requestID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^per"
|
|
||||||
},
|
|
||||||
"reply": {
|
|
||||||
"$ref": "#/components/schemas/PermissionV2Reply"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["sessionID", "requestID", "reply"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "type", "properties"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -15296,6 +15214,88 @@
|
|||||||
"required": ["id", "type", "properties"],
|
"required": ["id", "type", "properties"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["permission.v2.asked"]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^per"
|
||||||
|
},
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^ses"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/components/schemas/PermissionV2Source"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "sessionID", "action", "resources"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "type", "properties"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["permission.v2.replied"]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^ses"
|
||||||
|
},
|
||||||
|
"requestID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^per"
|
||||||
|
},
|
||||||
|
"reply": {
|
||||||
|
"$ref": "#/components/schemas/PermissionV2Reply"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["sessionID", "requestID", "reply"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "type", "properties"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -20175,27 +20175,6 @@
|
|||||||
"required": ["message", "isRetryable"],
|
"required": ["message", "isRetryable"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"PermissionV2Source": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["tool"]
|
|
||||||
},
|
|
||||||
"messageID": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"callID": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["type", "messageID", "callID"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"PermissionV2Reply": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["once", "always", "reject"]
|
|
||||||
},
|
|
||||||
"AuthOAuthCredential": {
|
"AuthOAuthCredential": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -20266,6 +20245,27 @@
|
|||||||
"required": ["id", "serviceID", "description", "credential"],
|
"required": ["id", "serviceID", "description", "credential"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"PermissionV2Source": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["tool"]
|
||||||
|
},
|
||||||
|
"messageID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"callID": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "messageID", "callID"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"PermissionV2Reply": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["once", "always", "reject"]
|
||||||
|
},
|
||||||
"QuestionV2Option": {
|
"QuestionV2Option": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -24737,88 +24737,6 @@
|
|||||||
"required": ["id", "type", "properties"],
|
"required": ["id", "type", "properties"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"EventPermissionV2Asked": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["permission.v2.asked"]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^per"
|
|
||||||
},
|
|
||||||
"sessionID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^ses"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"save": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"$ref": "#/components/schemas/PermissionV2Source"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "sessionID", "action", "resources"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "type", "properties"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"EventPermissionV2Replied": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["permission.v2.replied"]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"sessionID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^ses"
|
|
||||||
},
|
|
||||||
"requestID": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^per"
|
|
||||||
},
|
|
||||||
"reply": {
|
|
||||||
"$ref": "#/components/schemas/PermissionV2Reply"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["sessionID", "requestID", "reply"],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "type", "properties"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"EventFileEdited": {
|
"EventFileEdited": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -24921,6 +24839,88 @@
|
|||||||
"required": ["id", "type", "properties"],
|
"required": ["id", "type", "properties"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"EventPermissionV2Asked": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["permission.v2.asked"]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^per"
|
||||||
|
},
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^ses"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/components/schemas/PermissionV2Source"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "sessionID", "action", "resources"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "type", "properties"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"EventPermissionV2Replied": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["permission.v2.replied"]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sessionID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^ses"
|
||||||
|
},
|
||||||
|
"requestID": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^per"
|
||||||
|
},
|
||||||
|
"reply": {
|
||||||
|
"$ref": "#/components/schemas/PermissionV2Reply"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["sessionID", "requestID", "reply"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "type", "properties"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"EventFileWatcherUpdated": {
|
"EventFileWatcherUpdated": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -232,7 +232,10 @@ export interface Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly model: {
|
readonly model: {
|
||||||
readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
|
readonly get: (
|
||||||
|
providerID: ProviderV2.ID,
|
||||||
|
modelID: ModelV2.ID,
|
||||||
|
) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
|
||||||
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
readonly all: () => Effect.Effect<ModelV2.Info[]>
|
||||||
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
readonly available: () => Effect.Effect<ModelV2.Info[]>
|
||||||
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
|
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
|
||||||
|
|||||||
Reference in New Issue
Block a user