@f5-sales-demo/xcsh 20.8.6 → 20.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -8
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/terraform-index.generated.ts +90 -105
- package/src/routing/classifier.ts +6 -1
- package/src/routing/coordinator.ts +2 -0
- package/src/routing/delegation.ts +8 -2
- package/src/session/agent-session.ts +51 -270
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "20.
|
|
4
|
+
"version": "20.10.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"check:bundle": "bun scripts/check-bundle.ts",
|
|
41
41
|
"pretest": "bun ../../scripts/check-installed-dependencies.ts",
|
|
42
42
|
"test": "bun run generate-build-info && bun scripts/bun-test-guarded.ts --max-concurrency 1",
|
|
43
|
+
"bench:routing-matrix": "bun scripts/bench-routing-matrix.ts",
|
|
43
44
|
"fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index && bun run generate-api-spec-index && bun run generate-build-info",
|
|
44
45
|
"fmt": "biome format --write . && bun run format-prompts",
|
|
45
46
|
"format-prompts": "bun scripts/format-prompts.ts",
|
|
@@ -61,13 +62,13 @@
|
|
|
61
62
|
"dependencies": {
|
|
62
63
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
63
64
|
"@mozilla/readability": "^0.6",
|
|
64
|
-
"@f5-sales-demo/xcsh-stats": "20.
|
|
65
|
-
"@f5-sales-demo/pi-agent-core": "20.
|
|
66
|
-
"@f5-sales-demo/pi-ai": "20.
|
|
67
|
-
"@f5-sales-demo/pi-natives": "20.
|
|
68
|
-
"@f5-sales-demo/pi-resource-management": "20.
|
|
69
|
-
"@f5-sales-demo/pi-tui": "20.
|
|
70
|
-
"@f5-sales-demo/pi-utils": "20.
|
|
65
|
+
"@f5-sales-demo/xcsh-stats": "20.10.0",
|
|
66
|
+
"@f5-sales-demo/pi-agent-core": "20.10.0",
|
|
67
|
+
"@f5-sales-demo/pi-ai": "20.10.0",
|
|
68
|
+
"@f5-sales-demo/pi-natives": "20.10.0",
|
|
69
|
+
"@f5-sales-demo/pi-resource-management": "20.10.0",
|
|
70
|
+
"@f5-sales-demo/pi-tui": "20.10.0",
|
|
71
|
+
"@f5-sales-demo/pi-utils": "20.10.0",
|
|
71
72
|
"@sinclair/typebox": "^0.34",
|
|
72
73
|
"@xterm/headless": "^6.0",
|
|
73
74
|
"ajv": "^8.20",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "20.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.10.0",
|
|
21
|
+
"commit": "f6a647696fb10fb3e6ecccb35c753821f015741e",
|
|
22
|
+
"shortCommit": "f6a6476",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
24
|
+
"tag": "v20.10.0",
|
|
25
|
+
"commitDate": "2026-08-10T19:10:53Z",
|
|
26
|
+
"buildDate": "2026-08-10T19:39:51.013Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/f6a647696fb10fb3e6ecccb35c753821f015741e",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.10.0"
|
|
33
33
|
};
|
|
@@ -147,21 +147,6 @@ export const TERRAFORM_INDEX: TerraformIndex = {
|
|
|
147
147
|
"workload_flavor",
|
|
148
148
|
],
|
|
149
149
|
},
|
|
150
|
-
{
|
|
151
|
-
name: "DNS",
|
|
152
|
-
slug: "dns",
|
|
153
|
-
description: "DNS domains, zones, compliance checks, and DNS proxy configuration",
|
|
154
|
-
resource_count: 7,
|
|
155
|
-
resources: [
|
|
156
|
-
"dns_compliance_checks",
|
|
157
|
-
"dns_domain",
|
|
158
|
-
"dns_lb_health_check",
|
|
159
|
-
"dns_lb_pool",
|
|
160
|
-
"dns_load_balancer",
|
|
161
|
-
"dns_proxy",
|
|
162
|
-
"dns_zone",
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
150
|
{
|
|
166
151
|
name: "Uncategorized",
|
|
167
152
|
slug: "uncategorized",
|
|
@@ -177,6 +162,20 @@ export const TERRAFORM_INDEX: TerraformIndex = {
|
|
|
177
162
|
"registration_approval",
|
|
178
163
|
],
|
|
179
164
|
},
|
|
165
|
+
{
|
|
166
|
+
name: "DNS",
|
|
167
|
+
slug: "dns",
|
|
168
|
+
description: "DNS domains, zones, compliance checks, and DNS proxy configuration",
|
|
169
|
+
resource_count: 6,
|
|
170
|
+
resources: [
|
|
171
|
+
"dns_compliance_checks",
|
|
172
|
+
"dns_lb_health_check",
|
|
173
|
+
"dns_lb_pool",
|
|
174
|
+
"dns_load_balancer",
|
|
175
|
+
"dns_proxy",
|
|
176
|
+
"dns_zone",
|
|
177
|
+
],
|
|
178
|
+
},
|
|
180
179
|
{
|
|
181
180
|
name: "API Security",
|
|
182
181
|
slug: "api-security",
|
|
@@ -1523,201 +1522,187 @@ export const TERRAFORM_INDEX: TerraformIndex = {
|
|
|
1523
1522
|
import_syntax: "terraform import xcsh_workload_flavor.example namespace/name",
|
|
1524
1523
|
},
|
|
1525
1524
|
{
|
|
1526
|
-
name: "
|
|
1527
|
-
category: "
|
|
1528
|
-
description:
|
|
1529
|
-
"DNS Compliance Checks Specification in a given namespace. If one already exists it will give an error",
|
|
1525
|
+
name: "application_profiles",
|
|
1526
|
+
category: "uncategorized",
|
|
1527
|
+
description: "Application Profiles in a given namespace. If one already exists it will give an error",
|
|
1530
1528
|
required: ["name", "namespace"],
|
|
1531
1529
|
minimal_config: {
|
|
1532
1530
|
format: "terraform",
|
|
1533
|
-
source: "_llms-txt/resources/
|
|
1534
|
-
},
|
|
1535
|
-
dependencies: {
|
|
1536
|
-
requires: [],
|
|
1537
|
-
},
|
|
1538
|
-
import_syntax: "terraform import xcsh_dns_compliance_checks.example namespace/name",
|
|
1539
|
-
},
|
|
1540
|
-
{
|
|
1541
|
-
name: "dns_domain",
|
|
1542
|
-
category: "dns",
|
|
1543
|
-
description: "DNS Domain in a given namespace. If one already exist it will give a error",
|
|
1544
|
-
required: ["name", "dnssec_mode"],
|
|
1545
|
-
minimal_config: {
|
|
1546
|
-
format: "terraform",
|
|
1547
|
-
source: "_llms-txt/resources/dns_domain.txt#minimal-valid-config",
|
|
1531
|
+
source: "_llms-txt/resources/application_profiles.txt#minimal-valid-config",
|
|
1548
1532
|
},
|
|
1549
1533
|
dependencies: {
|
|
1550
1534
|
requires: [],
|
|
1551
1535
|
},
|
|
1552
|
-
import_syntax: "terraform import
|
|
1536
|
+
import_syntax: "terraform import xcsh_application_profiles.example namespace/name",
|
|
1553
1537
|
},
|
|
1554
1538
|
{
|
|
1555
|
-
name: "
|
|
1556
|
-
category: "
|
|
1557
|
-
description: "
|
|
1558
|
-
required: ["name"],
|
|
1539
|
+
name: "authorization_server",
|
|
1540
|
+
category: "uncategorized",
|
|
1541
|
+
description: "Authorization_server creates a new object in the storage backend for metadata.namespace",
|
|
1542
|
+
required: ["name", "namespace", "jwks_uri"],
|
|
1559
1543
|
minimal_config: {
|
|
1560
1544
|
format: "terraform",
|
|
1561
|
-
source: "_llms-txt/resources/
|
|
1545
|
+
source: "_llms-txt/resources/authorization_server.txt#minimal-valid-config",
|
|
1562
1546
|
},
|
|
1563
1547
|
dependencies: {
|
|
1564
1548
|
requires: [],
|
|
1565
1549
|
},
|
|
1566
|
-
import_syntax: "terraform import
|
|
1550
|
+
import_syntax: "terraform import xcsh_authorization_server.example namespace/name",
|
|
1567
1551
|
},
|
|
1568
1552
|
{
|
|
1569
|
-
name: "
|
|
1570
|
-
category: "
|
|
1571
|
-
description: "
|
|
1572
|
-
required: ["name", "
|
|
1553
|
+
name: "bot_infrastructure",
|
|
1554
|
+
category: "uncategorized",
|
|
1555
|
+
description: "Bot Infrastructure",
|
|
1556
|
+
required: ["name", "namespace", "traffic_type"],
|
|
1573
1557
|
minimal_config: {
|
|
1574
1558
|
format: "terraform",
|
|
1575
|
-
source: "_llms-txt/resources/
|
|
1559
|
+
source: "_llms-txt/resources/bot_infrastructure.txt#minimal-valid-config",
|
|
1576
1560
|
},
|
|
1577
1561
|
dependencies: {
|
|
1578
1562
|
requires: [],
|
|
1579
1563
|
},
|
|
1580
|
-
import_syntax: "terraform import
|
|
1564
|
+
import_syntax: "terraform import xcsh_bot_infrastructure.example namespace/name",
|
|
1581
1565
|
},
|
|
1582
1566
|
{
|
|
1583
|
-
name: "
|
|
1584
|
-
category: "
|
|
1585
|
-
description: "
|
|
1586
|
-
required: ["name"],
|
|
1567
|
+
name: "mitigated_domain",
|
|
1568
|
+
category: "uncategorized",
|
|
1569
|
+
description: "Mitigated Domain",
|
|
1570
|
+
required: ["name", "namespace", "mitigated_domain"],
|
|
1587
1571
|
minimal_config: {
|
|
1588
1572
|
format: "terraform",
|
|
1589
|
-
source: "_llms-txt/resources/
|
|
1573
|
+
source: "_llms-txt/resources/mitigated_domain.txt#minimal-valid-config",
|
|
1590
1574
|
},
|
|
1591
1575
|
dependencies: {
|
|
1592
1576
|
requires: [],
|
|
1593
1577
|
},
|
|
1594
|
-
import_syntax: "terraform import
|
|
1578
|
+
import_syntax: "terraform import xcsh_mitigated_domain.example namespace/name",
|
|
1595
1579
|
},
|
|
1596
1580
|
{
|
|
1597
|
-
name: "
|
|
1598
|
-
category: "
|
|
1599
|
-
description: "
|
|
1600
|
-
required: ["name", "
|
|
1581
|
+
name: "protected_application",
|
|
1582
|
+
category: "uncategorized",
|
|
1583
|
+
description: "Applications protected by Bot Defense",
|
|
1584
|
+
required: ["name", "namespace", "region"],
|
|
1601
1585
|
minimal_config: {
|
|
1602
1586
|
format: "terraform",
|
|
1603
|
-
source: "_llms-txt/resources/
|
|
1587
|
+
source: "_llms-txt/resources/protected_application.txt#minimal-valid-config",
|
|
1604
1588
|
},
|
|
1605
1589
|
dependencies: {
|
|
1606
1590
|
requires: [],
|
|
1607
1591
|
},
|
|
1608
|
-
import_syntax: "terraform import
|
|
1592
|
+
import_syntax: "terraform import xcsh_protected_application.example namespace/name",
|
|
1609
1593
|
},
|
|
1610
1594
|
{
|
|
1611
|
-
name: "
|
|
1612
|
-
category: "
|
|
1613
|
-
description: "
|
|
1614
|
-
required: ["name"],
|
|
1595
|
+
name: "protected_domain",
|
|
1596
|
+
category: "uncategorized",
|
|
1597
|
+
description: "Domain to protect",
|
|
1598
|
+
required: ["name", "namespace", "protected_domain"],
|
|
1615
1599
|
minimal_config: {
|
|
1616
1600
|
format: "terraform",
|
|
1617
|
-
source: "_llms-txt/resources/
|
|
1601
|
+
source: "_llms-txt/resources/protected_domain.txt#minimal-valid-config",
|
|
1618
1602
|
},
|
|
1619
1603
|
dependencies: {
|
|
1620
1604
|
requires: [],
|
|
1621
1605
|
},
|
|
1622
|
-
import_syntax: "terraform import
|
|
1606
|
+
import_syntax: "terraform import xcsh_protected_domain.example namespace/name",
|
|
1623
1607
|
},
|
|
1624
1608
|
{
|
|
1625
|
-
name: "
|
|
1609
|
+
name: "registration_approval",
|
|
1626
1610
|
category: "uncategorized",
|
|
1627
|
-
description: "
|
|
1611
|
+
description: "Request for admission approval. configuration",
|
|
1628
1612
|
required: ["name", "namespace"],
|
|
1629
1613
|
minimal_config: {
|
|
1630
1614
|
format: "terraform",
|
|
1631
|
-
source: "_llms-txt/resources/
|
|
1615
|
+
source: "_llms-txt/resources/registration_approval.txt#minimal-valid-config",
|
|
1632
1616
|
},
|
|
1633
1617
|
dependencies: {
|
|
1634
1618
|
requires: [],
|
|
1635
1619
|
},
|
|
1636
|
-
import_syntax: "terraform import
|
|
1620
|
+
import_syntax: "terraform import xcsh_registration_approval.example namespace/name",
|
|
1637
1621
|
},
|
|
1638
1622
|
{
|
|
1639
|
-
name: "
|
|
1640
|
-
category: "
|
|
1641
|
-
description:
|
|
1642
|
-
|
|
1623
|
+
name: "dns_compliance_checks",
|
|
1624
|
+
category: "dns",
|
|
1625
|
+
description:
|
|
1626
|
+
"DNS Compliance Checks Specification in a given namespace. If one already exists it will give an error",
|
|
1627
|
+
required: ["name", "namespace"],
|
|
1643
1628
|
minimal_config: {
|
|
1644
1629
|
format: "terraform",
|
|
1645
|
-
source: "_llms-txt/resources/
|
|
1630
|
+
source: "_llms-txt/resources/dns_compliance_checks.txt#minimal-valid-config",
|
|
1646
1631
|
},
|
|
1647
1632
|
dependencies: {
|
|
1648
1633
|
requires: [],
|
|
1649
1634
|
},
|
|
1650
|
-
import_syntax: "terraform import
|
|
1635
|
+
import_syntax: "terraform import xcsh_dns_compliance_checks.example namespace/name",
|
|
1651
1636
|
},
|
|
1652
1637
|
{
|
|
1653
|
-
name: "
|
|
1654
|
-
category: "
|
|
1655
|
-
description: "
|
|
1656
|
-
required: ["name"
|
|
1638
|
+
name: "dns_lb_health_check",
|
|
1639
|
+
category: "dns",
|
|
1640
|
+
description: "DNS Load Balancer Health Check in a given namespace. If one already exist it will give a error",
|
|
1641
|
+
required: ["name"],
|
|
1657
1642
|
minimal_config: {
|
|
1658
1643
|
format: "terraform",
|
|
1659
|
-
source: "_llms-txt/resources/
|
|
1644
|
+
source: "_llms-txt/resources/dns_lb_health_check.txt#minimal-valid-config",
|
|
1660
1645
|
},
|
|
1661
1646
|
dependencies: {
|
|
1662
1647
|
requires: [],
|
|
1663
1648
|
},
|
|
1664
|
-
import_syntax: "terraform import
|
|
1649
|
+
import_syntax: "terraform import xcsh_dns_lb_health_check.example namespace/name",
|
|
1665
1650
|
},
|
|
1666
1651
|
{
|
|
1667
|
-
name: "
|
|
1668
|
-
category: "
|
|
1669
|
-
description: "
|
|
1670
|
-
required: ["name", "
|
|
1652
|
+
name: "dns_lb_pool",
|
|
1653
|
+
category: "dns",
|
|
1654
|
+
description: "DNS Load Balancer Pool in a given namespace. If one already exist it will give a error",
|
|
1655
|
+
required: ["name", "load_balancing_mode"],
|
|
1671
1656
|
minimal_config: {
|
|
1672
1657
|
format: "terraform",
|
|
1673
|
-
source: "_llms-txt/resources/
|
|
1658
|
+
source: "_llms-txt/resources/dns_lb_pool.txt#minimal-valid-config",
|
|
1674
1659
|
},
|
|
1675
1660
|
dependencies: {
|
|
1676
1661
|
requires: [],
|
|
1677
1662
|
},
|
|
1678
|
-
import_syntax: "terraform import
|
|
1663
|
+
import_syntax: "terraform import xcsh_dns_lb_pool.example namespace/name",
|
|
1679
1664
|
},
|
|
1680
1665
|
{
|
|
1681
|
-
name: "
|
|
1682
|
-
category: "
|
|
1683
|
-
description: "
|
|
1684
|
-
required: ["name"
|
|
1666
|
+
name: "dns_load_balancer",
|
|
1667
|
+
category: "dns",
|
|
1668
|
+
description: "DNS Load Balancer in a given namespace. If one already exist it will give a error",
|
|
1669
|
+
required: ["name"],
|
|
1685
1670
|
minimal_config: {
|
|
1686
1671
|
format: "terraform",
|
|
1687
|
-
source: "_llms-txt/resources/
|
|
1672
|
+
source: "_llms-txt/resources/dns_load_balancer.txt#minimal-valid-config",
|
|
1688
1673
|
},
|
|
1689
1674
|
dependencies: {
|
|
1690
1675
|
requires: [],
|
|
1691
1676
|
},
|
|
1692
|
-
import_syntax: "terraform import
|
|
1677
|
+
import_syntax: "terraform import xcsh_dns_load_balancer.example namespace/name",
|
|
1693
1678
|
},
|
|
1694
1679
|
{
|
|
1695
|
-
name: "
|
|
1696
|
-
category: "
|
|
1697
|
-
description: "
|
|
1698
|
-
required: ["name", "
|
|
1680
|
+
name: "dns_proxy",
|
|
1681
|
+
category: "dns",
|
|
1682
|
+
description: "DNS Proxy in a given namespace. If one already exists it will give an error",
|
|
1683
|
+
required: ["name", "transport_type"],
|
|
1699
1684
|
minimal_config: {
|
|
1700
1685
|
format: "terraform",
|
|
1701
|
-
source: "_llms-txt/resources/
|
|
1686
|
+
source: "_llms-txt/resources/dns_proxy.txt#minimal-valid-config",
|
|
1702
1687
|
},
|
|
1703
1688
|
dependencies: {
|
|
1704
1689
|
requires: [],
|
|
1705
1690
|
},
|
|
1706
|
-
import_syntax: "terraform import
|
|
1691
|
+
import_syntax: "terraform import xcsh_dns_proxy.example namespace/name",
|
|
1707
1692
|
},
|
|
1708
1693
|
{
|
|
1709
|
-
name: "
|
|
1710
|
-
category: "
|
|
1711
|
-
description: "
|
|
1712
|
-
required: ["name"
|
|
1694
|
+
name: "dns_zone",
|
|
1695
|
+
category: "dns",
|
|
1696
|
+
description: "DNS Zone in a given namespace. If one already exist it will give a error",
|
|
1697
|
+
required: ["name"],
|
|
1713
1698
|
minimal_config: {
|
|
1714
1699
|
format: "terraform",
|
|
1715
|
-
source: "_llms-txt/resources/
|
|
1700
|
+
source: "_llms-txt/resources/dns_zone.txt#minimal-valid-config",
|
|
1716
1701
|
},
|
|
1717
1702
|
dependencies: {
|
|
1718
1703
|
requires: [],
|
|
1719
1704
|
},
|
|
1720
|
-
import_syntax: "terraform import
|
|
1705
|
+
import_syntax: "terraform import xcsh_dns_zone.example namespace/name",
|
|
1721
1706
|
},
|
|
1722
1707
|
{
|
|
1723
1708
|
name: "api_crawler",
|
|
@@ -86,7 +86,12 @@ export async function classifyTaskHybrid(options: HybridClassifierOptions): Prom
|
|
|
86
86
|
if (err instanceof Error && err.name === "AbortError") {
|
|
87
87
|
throw err;
|
|
88
88
|
}
|
|
89
|
-
const isSyntax =
|
|
89
|
+
const isSyntax =
|
|
90
|
+
err instanceof SyntaxError ||
|
|
91
|
+
(err instanceof Error &&
|
|
92
|
+
(err.message.includes("malformed") ||
|
|
93
|
+
err.message.includes("JSON") ||
|
|
94
|
+
err.message.includes("Unexpected token")));
|
|
90
95
|
return {
|
|
91
96
|
...baseProfile,
|
|
92
97
|
reasons: [...baseProfile.reasons, isSyntax ? "classifier_fallback_malformed" : "classifier_fallback_timeout"],
|
|
@@ -28,6 +28,8 @@ export interface EvaluateTurnOptions {
|
|
|
28
28
|
downshiftAfterTurns?: number;
|
|
29
29
|
getModelContextWindow?: (modelId: string) => number;
|
|
30
30
|
runRoutingClassifier?: (utilityModel: string, prompt: string) => Promise<string>;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
ttftStartNs?: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export class RoutingCoordinator {
|
|
@@ -41,7 +41,10 @@ export function validateDelegationPlan(plan: ReadOnlyDelegationPlan, maxTasks =
|
|
|
41
41
|
|
|
42
42
|
export async function executeReadOnlyDelegationPlan(
|
|
43
43
|
plan: ReadOnlyDelegationPlan,
|
|
44
|
-
executor: (
|
|
44
|
+
executor: (
|
|
45
|
+
subtaskPrompt: string,
|
|
46
|
+
options?: { signal?: AbortSignal; allowedTools?: (toolName: string) => boolean },
|
|
47
|
+
) => Promise<{ result: string; tokens: number }>,
|
|
45
48
|
maxTasks = 3,
|
|
46
49
|
options?: { signal?: AbortSignal },
|
|
47
50
|
): Promise<{ results: Array<{ id: string; result: string }>; tokensUsed: number }> {
|
|
@@ -65,7 +68,10 @@ export async function executeReadOnlyDelegationPlan(
|
|
|
65
68
|
]
|
|
66
69
|
.filter(Boolean)
|
|
67
70
|
.join("\n");
|
|
68
|
-
const { result, tokens } = await executor(promptPayload,
|
|
71
|
+
const { result, tokens } = await executor(promptPayload, {
|
|
72
|
+
signal: options?.signal,
|
|
73
|
+
allowedTools: isDelegationAllowedTool,
|
|
74
|
+
});
|
|
69
75
|
return { id: task.id, result, tokens };
|
|
70
76
|
} catch (err) {
|
|
71
77
|
return { id: task.id, result: `Failed: ${String(err)}`, tokens: 0 };
|
|
@@ -43,7 +43,6 @@ import type {
|
|
|
43
43
|
} from "@f5-sales-demo/pi-ai";
|
|
44
44
|
import {
|
|
45
45
|
calculateRateLimitBackoffMs,
|
|
46
|
-
completeSimple,
|
|
47
46
|
getSupportedEfforts,
|
|
48
47
|
isContextOverflow,
|
|
49
48
|
isUsageLimitError,
|
|
@@ -445,6 +444,7 @@ export class AgentSession {
|
|
|
445
444
|
* distinguishable from launch configuration (#2459). Defaults to "config": settings, a remembered
|
|
446
445
|
* role, or a resumed session are all config, and only --model or a live switch is not.
|
|
447
446
|
*/
|
|
447
|
+
#initialModelResolutionSource: ModelResolutionSource = "config";
|
|
448
448
|
#modelResolutionSource: ModelResolutionSource = "config";
|
|
449
449
|
#promptTemplates: PromptTemplate[];
|
|
450
450
|
#slashCommands: FileSlashCommand[];
|
|
@@ -596,7 +596,8 @@ export class AgentSession {
|
|
|
596
596
|
this.#pythonKernelOwnerId = config.pythonKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
597
597
|
this.#scopedModels = config.scopedModels ?? [];
|
|
598
598
|
this.#thinkingLevel = config.thinkingLevel;
|
|
599
|
-
this.#
|
|
599
|
+
this.#initialModelResolutionSource = config.modelResolutionSource ?? "config";
|
|
600
|
+
this.#modelResolutionSource = this.#initialModelResolutionSource;
|
|
600
601
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
601
602
|
this.#slashCommands = config.slashCommands ?? [];
|
|
602
603
|
this.#extensionRunner = config.extensionRunner;
|
|
@@ -1348,7 +1349,6 @@ export class AgentSession {
|
|
|
1348
1349
|
}
|
|
1349
1350
|
|
|
1350
1351
|
if (!outcome.safeToContinue) {
|
|
1351
|
-
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1352
1352
|
return;
|
|
1353
1353
|
}
|
|
1354
1354
|
|
|
@@ -1357,12 +1357,11 @@ export class AgentSession {
|
|
|
1357
1357
|
const previousModel = this.model;
|
|
1358
1358
|
|
|
1359
1359
|
try {
|
|
1360
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
1360
1361
|
this.agent.abort();
|
|
1361
1362
|
|
|
1362
1363
|
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1363
1364
|
this.#routingCoordinator.restoreState({ currentTier: targetTier });
|
|
1364
|
-
|
|
1365
|
-
await this.setModelRoutingSwitch(targetModel);
|
|
1366
1365
|
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
1367
1366
|
const { mapTierToEffort } = await import("../routing/effort");
|
|
1368
1367
|
const effort = mapTierToEffort(targetTier, effortMap);
|
|
@@ -2809,264 +2808,21 @@ export class AgentSession {
|
|
|
2809
2808
|
|
|
2810
2809
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2811
2810
|
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2811
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
2812
|
+
expandedText,
|
|
2813
|
+
options?.images ? options.images.length > 0 : false,
|
|
2814
|
+
{ signal: options?.signal },
|
|
2815
|
+
);
|
|
2816
|
+
if (delegationOutput) {
|
|
2817
|
+
expandedText += delegationOutput;
|
|
2818
|
+
const textBlock = userContent.find(c => c.type === "text") as Extract<
|
|
2819
|
+
(typeof userContent)[0],
|
|
2820
|
+
{ type: "text" }
|
|
2821
|
+
>;
|
|
2822
|
+
if (textBlock) {
|
|
2823
|
+
textBlock.text += delegationOutput;
|
|
2821
2824
|
} else {
|
|
2822
|
-
|
|
2823
|
-
const availableModels =
|
|
2824
|
-
this.#scopedModels.length > 0
|
|
2825
|
-
? this.#scopedModels.map(sm => `${sm.model.provider}/${sm.model.id}`)
|
|
2826
|
-
: this.#modelRegistry.getAvailable().map(m => `${m.provider}/${m.id}`);
|
|
2827
|
-
const startTime = Date.now();
|
|
2828
|
-
const systemTokens = Math.round((this.systemPrompt?.length ?? 0) / 4);
|
|
2829
|
-
const promptTokens = Math.round(expandedText.length / 4);
|
|
2830
|
-
const toolsStr = JSON.stringify(Array.from(this.#toolRegistry.values()));
|
|
2831
|
-
const toolsTokens = Math.round(toolsStr.length / 4);
|
|
2832
|
-
const reserveTokens = (this.settings.get("compaction.reserveTokens") as number) ?? 0;
|
|
2833
|
-
const contextEstimate = {
|
|
2834
|
-
usedTokens: calculateUsedTokens(this.messages) + systemTokens + promptTokens + toolsTokens,
|
|
2835
|
-
reserveTokens,
|
|
2836
|
-
contextWindow: this.model.contextWindow ?? 128000,
|
|
2837
|
-
};
|
|
2838
|
-
const decision = await this.#routingCoordinator.evaluateTurn({
|
|
2839
|
-
anchorModel,
|
|
2840
|
-
mode: routingMode,
|
|
2841
|
-
prompt: expandedText,
|
|
2842
|
-
hasImages: options?.images && options.images.length > 0,
|
|
2843
|
-
priorRejection: this.#routingCoordinator.getState().escalationFloor !== undefined,
|
|
2844
|
-
availableModels,
|
|
2845
|
-
customPools: validateCustomPools(this.settings.get("routing.pools")),
|
|
2846
|
-
disabledPresets: (this.settings.get("routing.disabledPresets") as readonly string[]) ?? [],
|
|
2847
|
-
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
2848
|
-
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
2849
|
-
contextEstimate,
|
|
2850
|
-
getModelContextWindow: (modelId: string) => {
|
|
2851
|
-
const found = this.#modelRegistry
|
|
2852
|
-
.getAll()
|
|
2853
|
-
.find(m => `${m.provider}/${m.id}` === modelId || m.id === modelId);
|
|
2854
|
-
return found?.contextWindow ?? 128000;
|
|
2855
|
-
},
|
|
2856
|
-
runRoutingClassifier: async (utilityModel: string, promptText: string) => {
|
|
2857
|
-
const resolved = this.#modelRegistry
|
|
2858
|
-
.getAll()
|
|
2859
|
-
.find(m => `${m.provider}/${m.id}` === utilityModel || m.id === utilityModel);
|
|
2860
|
-
if (!resolved) throw new Error("Classifier model not found");
|
|
2861
|
-
|
|
2862
|
-
let targetModel = resolved;
|
|
2863
|
-
if (
|
|
2864
|
-
resolved.provider === "openai" ||
|
|
2865
|
-
(resolved.provider === "litellm" && resolved.id.includes("openai"))
|
|
2866
|
-
) {
|
|
2867
|
-
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
2868
|
-
if (internalUrl) targetModel = { ...resolved, baseUrl: internalUrl };
|
|
2869
|
-
} else if (
|
|
2870
|
-
resolved.provider === "anthropic" ||
|
|
2871
|
-
(resolved.provider === "litellm" &&
|
|
2872
|
-
(resolved.id.includes("anthropic") || resolved.id.includes("claude")))
|
|
2873
|
-
) {
|
|
2874
|
-
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
2875
|
-
if (internalUrl) targetModel = { ...resolved, baseUrl: internalUrl };
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
const apiKey = await this.#modelRegistry.getApiKey(targetModel, this.sessionId);
|
|
2879
|
-
if (!apiKey) throw new Error("No API key for classifier model");
|
|
2880
|
-
|
|
2881
|
-
const systemInstruction = `You are a routing classifier. Analyze the user's prompt and output a JSON object: { "complexityScore": number, "confidence": number, "delegation"?: { "reason": string, "subtasks": { "id": string, "title": string, "description": string, "targetFilesOrPaths": string[], "desiredTier"?: string }[] } }. Score complexity from 0 to 100 based on required reasoning, context width, and coding effort. Return ONLY valid JSON without markdown blocks.`;
|
|
2882
|
-
|
|
2883
|
-
const res = await completeSimple(
|
|
2884
|
-
targetModel,
|
|
2885
|
-
{
|
|
2886
|
-
systemPrompt: systemInstruction,
|
|
2887
|
-
messages: [
|
|
2888
|
-
{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() },
|
|
2889
|
-
],
|
|
2890
|
-
},
|
|
2891
|
-
{ apiKey, signal: AbortSignal.timeout(15000) },
|
|
2892
|
-
);
|
|
2893
|
-
const rawText = res.content
|
|
2894
|
-
.filter(c => c.type === "text")
|
|
2895
|
-
.map(c => (c as TextContent).text)
|
|
2896
|
-
.join("");
|
|
2897
|
-
|
|
2898
|
-
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
2899
|
-
if (jsonMatch) {
|
|
2900
|
-
const parsed = JSON.parse(jsonMatch[0]); // Check validity
|
|
2901
|
-
if (res.usage) {
|
|
2902
|
-
this.settings.getStorage()?.recordModelUsage(`${resolved.provider}/${resolved.id}`);
|
|
2903
|
-
parsed.routingUsage = (res.usage as any).totalTokens ?? (res.usage as any).total ?? 0;
|
|
2904
|
-
}
|
|
2905
|
-
return JSON.stringify(parsed);
|
|
2906
|
-
}
|
|
2907
|
-
throw new Error("Classifier returned malformed JSON");
|
|
2908
|
-
},
|
|
2909
|
-
downshiftAfterTurns: (this.settings.get("routing.downshiftAfterTurns") as number) ?? 2,
|
|
2910
|
-
});
|
|
2911
|
-
const durationMs = Date.now() - startTime;
|
|
2912
|
-
|
|
2913
|
-
this.#emitSessionEvent({
|
|
2914
|
-
type: decision.applied ? "routing_applied" : "routing_decision",
|
|
2915
|
-
epochId: decision.epochId,
|
|
2916
|
-
mode: decision.mode,
|
|
2917
|
-
provider: this.model.provider,
|
|
2918
|
-
poolId: decision.poolId,
|
|
2919
|
-
effectiveTier: decision.effectiveTier,
|
|
2920
|
-
selectedModel: decision.selectedModel,
|
|
2921
|
-
reasons: decision.reasons,
|
|
2922
|
-
delegated: !!decision.delegation,
|
|
2923
|
-
escalated: decision.reasons.some(
|
|
2924
|
-
r => r === "context_capacity_promotion" || r === "escalation_floor_active",
|
|
2925
|
-
),
|
|
2926
|
-
contextTokens: contextEstimate.usedTokens,
|
|
2927
|
-
routingUsage: decision.routingUsage,
|
|
2928
|
-
durationMs,
|
|
2929
|
-
state: this.#routingCoordinator.getState(),
|
|
2930
|
-
} as any).catch(() => {});
|
|
2931
|
-
|
|
2932
|
-
if (decision.selectedModel && decision.applied) {
|
|
2933
|
-
const targetModel = this.#modelRegistry
|
|
2934
|
-
.getAvailable()
|
|
2935
|
-
.find(m => `${m.provider}/${m.id}` === decision.selectedModel || m.id === decision.selectedModel);
|
|
2936
|
-
if (targetModel) {
|
|
2937
|
-
const currentModelId = this.model ? `${this.model.provider}/${this.model.id}` : undefined;
|
|
2938
|
-
const targetModelId = `${targetModel.provider}/${targetModel.id}`;
|
|
2939
|
-
if (currentModelId !== targetModelId) {
|
|
2940
|
-
await this.setModelRoutingSwitch(targetModel);
|
|
2941
|
-
}
|
|
2942
|
-
if (decision.effectiveTier) {
|
|
2943
|
-
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
2944
|
-
const { mapTierToEffort } = await import("../routing/effort");
|
|
2945
|
-
const effort = mapTierToEffort(decision.effectiveTier, effortMap);
|
|
2946
|
-
this.setThinkingLevel(effort as any);
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
|
|
2951
|
-
if (
|
|
2952
|
-
decision.applied &&
|
|
2953
|
-
decision.delegation &&
|
|
2954
|
-
decision.delegation.subtasks.length > 1 &&
|
|
2955
|
-
(this.settings.get("routing.delegation") as string) === "read-only"
|
|
2956
|
-
) {
|
|
2957
|
-
let maxTasks = (this.settings.get("routing.delegationMaxTasks") as number) ?? 3;
|
|
2958
|
-
maxTasks = Math.min(Math.max(1, maxTasks), 3);
|
|
2959
|
-
|
|
2960
|
-
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
2961
|
-
decision.delegation,
|
|
2962
|
-
async (subtaskPrompt, signal) => {
|
|
2963
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2964
|
-
|
|
2965
|
-
const { resolveModelPool } = await import("../routing/presets");
|
|
2966
|
-
const pool = resolveModelPool(
|
|
2967
|
-
decision.poolId ?? (this.model ? `${this.model.provider}/${this.model.id}` : ""),
|
|
2968
|
-
this.settings.get("routing.pools") as any,
|
|
2969
|
-
this.settings.get("routing.disabledPresets") as readonly string[],
|
|
2970
|
-
this.settings.get("routing.familyPolicy") as any,
|
|
2971
|
-
);
|
|
2972
|
-
|
|
2973
|
-
let resolvedUtility = this.model;
|
|
2974
|
-
if (pool?.tiers?.utility) {
|
|
2975
|
-
let uId = pool.tiers.utility;
|
|
2976
|
-
if (!uId.includes("/") && pool.provider) {
|
|
2977
|
-
uId = `${pool.provider}/${uId}`;
|
|
2978
|
-
}
|
|
2979
|
-
const found = this.#modelRegistry
|
|
2980
|
-
.getAvailable()
|
|
2981
|
-
.find(m => `${m.provider}/${m.id}` === uId || m.id === uId);
|
|
2982
|
-
if (found) resolvedUtility = found;
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
if (!resolvedUtility) return { result: "Delegation failed: no model.", tokens: 0 };
|
|
2986
|
-
|
|
2987
|
-
const { createAgentSession } = await import("../sdk");
|
|
2988
|
-
const { SessionManager: SDKSessionManager } = await import("./session-manager");
|
|
2989
|
-
const childSettings = await this.settings.cloneForCwd(process.cwd());
|
|
2990
|
-
childSettings.set("routing.delegation", "off");
|
|
2991
|
-
|
|
2992
|
-
// Fix abort race by checking right before creation
|
|
2993
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2994
|
-
|
|
2995
|
-
const { session: childSession } = await createAgentSession({
|
|
2996
|
-
model: resolvedUtility,
|
|
2997
|
-
sessionManager: SDKSessionManager.inMemory(),
|
|
2998
|
-
settings: childSettings,
|
|
2999
|
-
authStorage: this.#modelRegistry.authStorage,
|
|
3000
|
-
modelRegistry: this.#modelRegistry,
|
|
3001
|
-
toolNames: Array.from(this.#toolRegistry.values())
|
|
3002
|
-
.map(t => t.name)
|
|
3003
|
-
.filter(isDelegationAllowedTool),
|
|
3004
|
-
enableLsp: false,
|
|
3005
|
-
enableMCP: false,
|
|
3006
|
-
});
|
|
3007
|
-
|
|
3008
|
-
try {
|
|
3009
|
-
const onAbort = () => childSession.agent.abort();
|
|
3010
|
-
signal?.addEventListener("abort", onAbort);
|
|
3011
|
-
if (signal?.aborted) {
|
|
3012
|
-
onAbort();
|
|
3013
|
-
return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3014
|
-
}
|
|
3015
|
-
try {
|
|
3016
|
-
await childSession.prompt(subtaskPrompt);
|
|
3017
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3018
|
-
const resultStr = childSession
|
|
3019
|
-
.buildDisplaySessionContext()
|
|
3020
|
-
.messages.filter(m => m.role === "assistant")
|
|
3021
|
-
.map(m =>
|
|
3022
|
-
m.content
|
|
3023
|
-
.filter((c: any) => c.type === "text")
|
|
3024
|
-
.map((c: any) => c.text)
|
|
3025
|
-
.join(""),
|
|
3026
|
-
)
|
|
3027
|
-
.join("\n");
|
|
3028
|
-
const tokens = childSession.buildDisplaySessionContext().usedTokens || 0;
|
|
3029
|
-
return { result: resultStr, tokens };
|
|
3030
|
-
} finally {
|
|
3031
|
-
signal?.removeEventListener("abort", onAbort);
|
|
3032
|
-
}
|
|
3033
|
-
} catch (err) {
|
|
3034
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3035
|
-
return { result: `Delegation failed: ${String(err)}`, tokens: 0 };
|
|
3036
|
-
} finally {
|
|
3037
|
-
await childSession.dispose();
|
|
3038
|
-
}
|
|
3039
|
-
},
|
|
3040
|
-
maxTasks,
|
|
3041
|
-
{ signal: options?.signal },
|
|
3042
|
-
);
|
|
3043
|
-
|
|
3044
|
-
if (results.length > 0) {
|
|
3045
|
-
let resultsString = JSON.stringify(results, null, 2);
|
|
3046
|
-
if (resultsString.length > 8000) {
|
|
3047
|
-
resultsString = `${resultsString.substring(0, 8000)}\n...[truncated]`;
|
|
3048
|
-
}
|
|
3049
|
-
const delegationOutput = `\n\n<delegation_results>\n${resultsString}\n</delegation_results>`;
|
|
3050
|
-
expandedText += delegationOutput;
|
|
3051
|
-
const textBlock = userContent.find(c => c.type === "text") as Extract<
|
|
3052
|
-
(typeof userContent)[0],
|
|
3053
|
-
{ type: "text" }
|
|
3054
|
-
>;
|
|
3055
|
-
if (textBlock) {
|
|
3056
|
-
textBlock.text += delegationOutput;
|
|
3057
|
-
} else {
|
|
3058
|
-
userContent.push({ type: "text", text: delegationOutput });
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
this.#emitSessionEvent({
|
|
3062
|
-
type: "routing_delegated",
|
|
3063
|
-
epochId: decision.epochId,
|
|
3064
|
-
tasks: decision.delegation.subtasks.length,
|
|
3065
|
-
completed: results.length,
|
|
3066
|
-
tokensUsed,
|
|
3067
|
-
} as any).catch(() => {});
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
2825
|
+
userContent.push({ type: "text", text: delegationOutput });
|
|
3070
2826
|
}
|
|
3071
2827
|
}
|
|
3072
2828
|
|
|
@@ -3087,7 +2843,7 @@ export class AgentSession {
|
|
|
3087
2843
|
|
|
3088
2844
|
async promptCustomMessage<T = unknown>(
|
|
3089
2845
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3090
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
|
|
2846
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "signal">,
|
|
3091
2847
|
): Promise<void> {
|
|
3092
2848
|
const textContent =
|
|
3093
2849
|
typeof message.content === "string"
|
|
@@ -3122,6 +2878,7 @@ export class AgentSession {
|
|
|
3122
2878
|
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3123
2879
|
textContent,
|
|
3124
2880
|
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
2881
|
+
{ signal: options?.signal },
|
|
3125
2882
|
);
|
|
3126
2883
|
if (delegationOutput) {
|
|
3127
2884
|
if (typeof customMessage.content === "string") {
|
|
@@ -3184,6 +2941,7 @@ export class AgentSession {
|
|
|
3184
2941
|
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
3185
2942
|
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
3186
2943
|
contextEstimate,
|
|
2944
|
+
signal: options?.signal,
|
|
3187
2945
|
getModelContextWindow: (modelId: string) => {
|
|
3188
2946
|
const m = this.#modelRegistry
|
|
3189
2947
|
.getAvailable()
|
|
@@ -3218,7 +2976,8 @@ export class AgentSession {
|
|
|
3218
2976
|
if (!needsSwitch) {
|
|
3219
2977
|
if (
|
|
3220
2978
|
targetModel.provider === "openai" ||
|
|
3221
|
-
(targetModel.provider === "litellm" &&
|
|
2979
|
+
(targetModel.provider === "litellm" &&
|
|
2980
|
+
(targetModel.id.includes("openai") || /gpt-/.test(targetModel.id)))
|
|
3222
2981
|
) {
|
|
3223
2982
|
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
3224
2983
|
if ((internalUrl || undefined) !== this.model.baseUrl) {
|
|
@@ -3253,7 +3012,9 @@ export class AgentSession {
|
|
|
3253
3012
|
|
|
3254
3013
|
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
3255
3014
|
decision.delegation,
|
|
3256
|
-
async (subtaskPrompt,
|
|
3015
|
+
async (subtaskPrompt, options) => {
|
|
3016
|
+
const signal = options?.signal;
|
|
3017
|
+
const allowedTools = options?.allowedTools ?? isDelegationAllowedTool;
|
|
3257
3018
|
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3258
3019
|
|
|
3259
3020
|
const { resolveModelPool } = await import("../routing/presets");
|
|
@@ -3280,7 +3041,8 @@ export class AgentSession {
|
|
|
3280
3041
|
|
|
3281
3042
|
if (
|
|
3282
3043
|
resolvedUtility.provider === "openai" ||
|
|
3283
|
-
(resolvedUtility.provider === "litellm" &&
|
|
3044
|
+
(resolvedUtility.provider === "litellm" &&
|
|
3045
|
+
(resolvedUtility.id.includes("openai") || /gpt-/.test(resolvedUtility.id)))
|
|
3284
3046
|
) {
|
|
3285
3047
|
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
3286
3048
|
if (internalUrl) resolvedUtility = { ...resolvedUtility, baseUrl: internalUrl } as any;
|
|
@@ -3308,7 +3070,7 @@ export class AgentSession {
|
|
|
3308
3070
|
modelRegistry: this.#modelRegistry,
|
|
3309
3071
|
toolNames: Array.from(this.#toolRegistry.values())
|
|
3310
3072
|
.map(t => t.name)
|
|
3311
|
-
.filter(
|
|
3073
|
+
.filter(allowedTools),
|
|
3312
3074
|
enableLsp: false,
|
|
3313
3075
|
enableMCP: false,
|
|
3314
3076
|
});
|
|
@@ -3333,7 +3095,7 @@ export class AgentSession {
|
|
|
3333
3095
|
.join(""),
|
|
3334
3096
|
)
|
|
3335
3097
|
.join("\n");
|
|
3336
|
-
const tUsage = (
|
|
3098
|
+
const tUsage = childSession.buildDisplaySessionContext().usedTokens || 0;
|
|
3337
3099
|
return { result: resultStr || "No output", tokens: tUsage };
|
|
3338
3100
|
} finally {
|
|
3339
3101
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -3822,7 +3584,7 @@ export class AgentSession {
|
|
|
3822
3584
|
*/
|
|
3823
3585
|
async sendCustomMessage<T = unknown>(
|
|
3824
3586
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3825
|
-
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
3587
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; signal?: AbortSignal },
|
|
3826
3588
|
): Promise<void> {
|
|
3827
3589
|
const clonedContent = Array.isArray(message.content)
|
|
3828
3590
|
? message.content.map((c: any) => ({ ...c }))
|
|
@@ -4367,7 +4129,10 @@ export class AgentSession {
|
|
|
4367
4129
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
4368
4130
|
|
|
4369
4131
|
let targetModel = model;
|
|
4370
|
-
if (
|
|
4132
|
+
if (
|
|
4133
|
+
model.provider === "openai" ||
|
|
4134
|
+
(model.provider === "litellm" && (model.id.includes("openai") || /gpt-/.test(model.id)))
|
|
4135
|
+
) {
|
|
4371
4136
|
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
4372
4137
|
if (internalUrl) targetModel = { ...model, baseUrl: internalUrl };
|
|
4373
4138
|
} else if (
|
|
@@ -6981,8 +6746,10 @@ export class AgentSession {
|
|
|
6981
6746
|
#syncRoutingStateFromBranch() {
|
|
6982
6747
|
this.#routingCoordinator.reset();
|
|
6983
6748
|
const entries = this.sessionManager.getBranch();
|
|
6749
|
+
let foundRoutingState = false;
|
|
6984
6750
|
for (const entry of entries) {
|
|
6985
6751
|
if (entry.type === "custom" && entry.customType === "routing_event") {
|
|
6752
|
+
foundRoutingState = true;
|
|
6986
6753
|
const event = entry.data as any;
|
|
6987
6754
|
if (event.state) {
|
|
6988
6755
|
this.#routingCoordinator.restoreState(event.state);
|
|
@@ -7001,6 +6768,16 @@ export class AgentSession {
|
|
|
7001
6768
|
}
|
|
7002
6769
|
}
|
|
7003
6770
|
}
|
|
6771
|
+
if (!foundRoutingState) {
|
|
6772
|
+
this.#modelResolutionSource = this.#initialModelResolutionSource;
|
|
6773
|
+
const hasServiceTierEntry = this.sessionManager
|
|
6774
|
+
.getBranch()
|
|
6775
|
+
.some(entry => entry.type === "service_tier_change");
|
|
6776
|
+
if (!hasServiceTierEntry) {
|
|
6777
|
+
const configuredServiceTier = this.settings?.get("serviceTier");
|
|
6778
|
+
this.agent.serviceTier = configuredServiceTier === "none" ? undefined : configuredServiceTier;
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
7004
6781
|
}
|
|
7005
6782
|
|
|
7006
6783
|
/**
|
|
@@ -7054,6 +6831,7 @@ export class AgentSession {
|
|
|
7054
6831
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
7055
6832
|
}
|
|
7056
6833
|
this.#syncTodoPhasesFromBranch();
|
|
6834
|
+
this.#syncRoutingStateFromBranch();
|
|
7057
6835
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
7058
6836
|
|
|
7059
6837
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
@@ -7600,6 +7378,9 @@ export class AgentSession {
|
|
|
7600
7378
|
export function calculateUsedTokens(messages: any[]): number {
|
|
7601
7379
|
const countTextLength = (content: any): number => {
|
|
7602
7380
|
if (typeof content === "string") {
|
|
7381
|
+
if (content.startsWith("data:image/") || content.startsWith("base64,")) {
|
|
7382
|
+
return 0;
|
|
7383
|
+
}
|
|
7603
7384
|
return content.length;
|
|
7604
7385
|
}
|
|
7605
7386
|
if (Array.isArray(content)) {
|