Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ jobs:
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 10
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down
50 changes: 36 additions & 14 deletions apps/server/src/provider/Drivers/PiDriver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PiSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Cache from "effect/Cache";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
Expand All @@ -12,7 +13,12 @@ import { makePiTextGeneration } from "../../textGeneration/PiTextGeneration.ts";
import { ServerConfig } from "../../config.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makePiAdapter } from "../Layers/PiAdapter.ts";
import { checkPiProviderStatus, makePendingPiProvider } from "../Layers/PiProvider.ts";
import {
checkPiProviderStatus,
enrichPiSnapshot,
makePendingPiProvider,
probePiCapabilities,
} from "../Layers/PiProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
Expand All @@ -23,16 +29,16 @@ import {
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makePackageManagedProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import { makePiContinuationGroupKey } from "./PiHome.ts";
import { makePiCapabilitiesCacheKey, makePiContinuationGroupKey } from "./PiHome.ts";

const decodePiSettings = Schema.decodeSync(PiSettings);

const DRIVER_KIND = ProviderDriverKind.make("piAgent");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
const CAPABILITIES_PROBE_TTL = Duration.minutes(5);

const UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
Expand Down Expand Up @@ -76,7 +82,7 @@ export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv> = {
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const httpClient = yield* HttpClient.HttpClient;
const eventLoggers = yield* ProviderEventLoggers;
const _eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
Expand Down Expand Up @@ -106,16 +112,23 @@ export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv> = {
});
const textGeneration = yield* makePiTextGeneration(effectiveConfig, processEnv);

const capabilitiesProbeCache = yield* Cache.make({
capacity: 1,
timeToLive: CAPABILITIES_PROBE_TTL,
lookup: () =>
probePiCapabilities(effectiveConfig, serverConfig.cwd, processEnv).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Path.Path, path),
),
});
const capabilitiesCacheKey = yield* makePiCapabilitiesCacheKey(effectiveConfig);

const checkProvider = checkPiProviderStatus(
effectiveConfig,
serverConfig.cwd,
() => Cache.get(capabilitiesProbeCache, capabilitiesCacheKey),
processEnv,
).pipe(
Effect.map(stampIdentity),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Path.Path, path),
);
).pipe(Effect.map(stampIdentity));

const snapshot = yield* makeManagedServerProvider<PiSettings>({
maintenanceCapabilities,
Expand All @@ -126,9 +139,18 @@ export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv> = {
makePendingPiProvider(settings).pipe(Effect.map(stampIdentity)),
checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
enrichPiSnapshot({
settings: effectiveConfig,
environment: processEnv,
snapshot,
maintenanceCapabilities,
publishSnapshot,
stampIdentity,
httpClient,
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Path.Path, path),
),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).pipe(
Expand Down
124 changes: 112 additions & 12 deletions apps/server/src/provider/Layers/PiAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
type CanonicalItemType,
EventId,
type PiSettings,
type ProviderApprovalDecision,
ProviderDriverKind,
ProviderInstanceId,
type ProviderRuntimeEvent,
type ProviderRuntimeTurnStatus,
type ProviderSendTurnInput,
type ProviderSession,
type ProviderUserInputAnswers,
RuntimeItemId,
Expand Down Expand Up @@ -116,6 +114,8 @@
readonly turnId: TurnId;
readonly startedAt: string;
readonly items: Array<PiToolItem>;
activeTextItemId: RuntimeItemId | undefined;
activeReasoningItemId: RuntimeItemId | undefined;
}

interface PendingExtensionUI {
Expand Down Expand Up @@ -216,7 +216,7 @@

function tryParseJsonObject(text: string): Record<string, unknown> | null {
try {
// eslint-disable-next-line no-restricted-syntax -- non-Effect context, no schema needed

Check warning on line 219 in apps/server/src/provider/Layers/PiAdapter.ts

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

oxlint

Unused eslint-disable directive (no problems were reported).
const v = JSON.parse(text) as unknown; // @effect-diagnostics-ignore preferSchemaOverJson
return v !== null && typeof v === "object" ? (v as Record<string, unknown>) : null;
} catch {
Expand Down Expand Up @@ -296,6 +296,39 @@
const turnState = context.turnState;
if (!turnState) return;

if (turnState.activeTextItemId) {
const closingTextId = turnState.activeTextItemId;
turnState.activeTextItemId = undefined;
const closingStamp = yield* makeEventStamp();
yield* offerRuntimeEvent({
eventId: closingStamp.eventId,
provider: PROVIDER,
createdAt: closingStamp.createdAt,
threadId: context.session.threadId,
turnId: turnState.turnId,
itemId: closingTextId,
type: "item.completed",
payload: { itemType: "assistant_message", status: "completed" },
providerRefs: {},
});
}
if (turnState.activeReasoningItemId) {
const closingReasoningId = turnState.activeReasoningItemId;
turnState.activeReasoningItemId = undefined;
const closingStamp = yield* makeEventStamp();
yield* offerRuntimeEvent({
eventId: closingStamp.eventId,
provider: PROVIDER,
createdAt: closingStamp.createdAt,
threadId: context.session.threadId,
turnId: turnState.turnId,
itemId: closingReasoningId,
type: "item.completed",
payload: { itemType: "reasoning", status: "completed" },
providerRefs: {},
});
}

context.turnState = undefined;
context.turns.push({
id: turnState.turnId,
Expand Down Expand Up @@ -358,7 +391,13 @@
if (!context.turnState) {
const turnId = TurnId.make(yield* Random.nextUUIDv4);
const startedAt = yield* nowIso;
context.turnState = { turnId, startedAt, items: [] };
context.turnState = {
turnId,
startedAt,
items: [],
activeTextItemId: undefined,
activeReasoningItemId: undefined,
};
const updatedAt = yield* nowIso;
context.session = {
...context.session,
Expand All @@ -381,19 +420,43 @@
const assistantEvent = event.assistantMessageEvent;
if (!assistantEvent) return;
if (assistantEvent.type === "text_delta") {
if (!context.turnState.activeTextItemId) {
const textItemId = RuntimeItemId.make(yield* Random.nextUUIDv4);
context.turnState.activeTextItemId = textItemId;
yield* offerRuntimeEvent({
...base,
turnId: context.turnState.turnId,
itemId: textItemId,
type: "item.started",
payload: { itemType: "assistant_message" },
});
}
yield* offerRuntimeEvent({
...base,
turnId: context.turnState.turnId,
itemId: context.turnState.activeTextItemId,
type: "content.delta",
payload: {
streamKind: "assistant_text",
delta: assistantEvent.delta,
},
});
} else if (assistantEvent.type === "thinking_delta") {
if (!context.turnState.activeReasoningItemId) {
const reasoningItemId = RuntimeItemId.make(yield* Random.nextUUIDv4);
context.turnState.activeReasoningItemId = reasoningItemId;
yield* offerRuntimeEvent({
...base,
turnId: context.turnState.turnId,
itemId: reasoningItemId,
type: "item.started",
payload: { itemType: "reasoning" },
});
}
yield* offerRuntimeEvent({
...base,
turnId: context.turnState.turnId,
itemId: context.turnState.activeReasoningItemId,
type: "content.delta",
payload: {
streamKind: "reasoning_text",
Expand All @@ -406,6 +469,28 @@

case "tool_execution_start": {
if (!context.turnState) return;
if (context.turnState.activeTextItemId) {
const closingTextId = context.turnState.activeTextItemId;
context.turnState.activeTextItemId = undefined;
yield* offerRuntimeEvent({
...base,
turnId: context.turnState.turnId,
itemId: closingTextId,
type: "item.completed",
payload: { itemType: "assistant_message", status: "completed" },
});
}
if (context.turnState.activeReasoningItemId) {
const closingReasoningId = context.turnState.activeReasoningItemId;
context.turnState.activeReasoningItemId = undefined;
yield* offerRuntimeEvent({
...base,
turnId: context.turnState.turnId,
itemId: closingReasoningId,
type: "item.completed",
payload: { itemType: "reasoning", status: "completed" },
});
}
const itemId = RuntimeItemId.make(event.toolCallId);
const itemType = classifyToolItemType(event.toolName);
const detail = summarizePiToolArgs(event.args);
Expand Down Expand Up @@ -1024,7 +1109,13 @@

const turnId = TurnId.make(yield* Random.nextUUIDv4);
const turnStartedAt = yield* nowIso;
context.turnState = { turnId, startedAt: turnStartedAt, items: [] };
context.turnState = {
turnId,
startedAt: turnStartedAt,
items: [],
activeTextItemId: undefined,
activeReasoningItemId: undefined,
};
context.session = {
...context.session,
status: "running",
Expand All @@ -1044,15 +1135,24 @@
providerRefs: {},
});

const promptText = typeof input.input === "string" ? input.input : "";
const rawPromptText = typeof input.input === "string" ? input.input : "";
const trimmed = rawPromptText.trim();

yield* context
.writeCommand({
type: "prompt",
message: promptText,
...(piImages.length > 0 ? { images: piImages } : {}),
})
.pipe(Effect.catchDefect(() => completeTurn(context, "failed", "Failed to send prompt.")));
if (trimmed === "/compact" || trimmed.startsWith("/compact ")) {
const customInstructions = trimmed.slice("/compact".length).trim() || undefined;
yield* context
.writeCommand({ type: "compact", ...(customInstructions ? { customInstructions } : {}) })
.pipe(Effect.catchDefect(() => completeTurn(context, "failed", "Compaction failed.")));
} else {
const promptText = rawPromptText.replace(/^\$([a-zA-Z][\w:.-]*)/, "/skill:$1");
yield* context
.writeCommand({
type: "prompt",
message: promptText,
...(piImages.length > 0 ? { images: piImages } : {}),
})
.pipe(Effect.catchDefect(() => completeTurn(context, "failed", "Failed to send prompt.")));
}

return {
threadId: context.session.threadId,
Expand Down
Loading