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: 1 addition & 1 deletion apps/api/src/schemas/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const TaskActivitySubstateSchema = z
.describe("Activity sub-state for running tasks");

export const AgentTypeSchema = z
.enum(["claude-code", "codex", "copilot", "opencode"])
.enum(["claude-code", "codex", "copilot", "opencode", "gemini", "openclaw"])
.describe("Agent runtime that executes the task");

export const WorktreeStateSchema = z
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/services/gemini-event-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ export function parseGeminiEvent(

// Tool result
if (event.type === "tool_result") {
const output = typeof event.output === "string" ? event.output : JSON.stringify(event.output);
const output =
typeof event.output === "string" ? event.output : (JSON.stringify(event.output) ?? "");
const trimmed = output.length > 300 ? output.slice(0, 300) + "\u2026" : output;
if (trimmed.trim()) {
entries.push({
Expand Down
13 changes: 8 additions & 5 deletions apps/api/src/workers/task-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,7 @@ export function buildAgentCommand(
? ` -m ${JSON.stringify(env.OPTIO_GEMINI_MODEL)}`
: "";
return [
`echo "[optio] Running Google Gemini${opts?.isReview ? " (review)" : ""}..."`,
`echo "[optio] Running Gemini..."`,
`gemini -p "$OPTIO_PROMPT" \\`,
` --output-format stream-json \\`,
` --approval-mode yolo${geminiModelFlag}`,
Expand Down Expand Up @@ -1842,12 +1842,15 @@ export function inferExitCode(agentType: string, logs: string): number {
}
case "gemini": {
const hasErrorEvent = logs.includes('"type":"error"') || logs.includes('"type": "error"');
const hasAuthError = /GEMINI_API_KEY|GOOGLE_API_KEY|permission denied|unauthorized/i.test(
logs,
);
// Match auth errors by error-descriptive patterns only (not bare env var names which
// could appear in diagnostic output and cause false positives).
const hasAuthError =
/api.?key.*(?:invalid|not valid|missing)|invalid.*api.?key|api_key_invalid|permission denied|unauthorized/i.test(
logs,
);
const hasQuotaError = /quota|resource.?exhausted|rate.?limit/i.test(logs);
const hasModelError = /model.*not found|model_not_found|does not exist.*model/i.test(logs);
const hasTurnLimit = /turn.?limit|exit code 53/i.test(logs);
const hasTurnLimit = /turn.?limit|exit:?\s*53\b/i.test(logs);
return hasErrorEvent || hasAuthError || hasQuotaError || hasModelError || hasTurnLimit
? 1
: 0;
Expand Down
1 change: 1 addition & 0 deletions helm/optio/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apiVersion: v2
name: optio
description: AI Agent Workflow Orchestration
kubeVersion: ">=1.33.0-0"
type: application
version: 0.1.0
appVersion: "0.1.0"
Expand Down
1 change: 1 addition & 0 deletions helm/optio/templates/api-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ spec:
ports:
- port: {{ .Values.api.port }}
targetPort: {{ .Values.api.port }}
appProtocol: kubernetes.io/ws # WebSocket support for /ws/* endpoints
{{- if and (eq .Values.api.service.type "NodePort") .Values.api.service.nodePort }}
nodePort: {{ .Values.api.service.nodePort }}
{{- end }}
2 changes: 1 addition & 1 deletion helm/optio/templates/gateway.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{- include "optio.validateNetworking" . }}
{{- if and .Values.gatewayAPI.enabled (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") }}
{{- if and .Values.gatewayAPI.enabled (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") (not .Values.gatewayAPI.parentRefs) }}
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
Expand Down
4 changes: 4 additions & 0 deletions helm/optio/templates/httproute.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ metadata:
{{- end }}
spec:
parentRefs:
{{- if .Values.gatewayAPI.parentRefs }}
{{- toYaml .Values.gatewayAPI.parentRefs | nindent 4 }}
{{- else }}
- name: {{ .Release.Name }}-gateway
namespace: {{ .Values.namespace }}
{{- end }}
{{- if .Values.gatewayAPI.hostname }}
hostnames:
- {{ .Values.gatewayAPI.hostname | quote }}
Expand Down
42 changes: 34 additions & 8 deletions images/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,55 @@ set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

# Parse arguments
TAG="${1:-latest}"
PLATFORM=""

# Check for --platform flag in any position
i=0
next_is_platform=false
for arg in "$@"; do
i=$((i + 1))
if [[ "$next_is_platform" == true ]]; then
PLATFORM="$arg"
next_is_platform=false
elif [[ "$arg" == --platform=* ]]; then
PLATFORM="${arg#*=}"
elif [[ "$arg" == "--platform" ]]; then
next_is_platform=true
fi
done

# Build platform flag if specified
PLATFORM_FLAG=""
if [ -n "${PLATFORM}" ]; then
PLATFORM_FLAG="--platform ${PLATFORM}"
echo "Building for platform: ${PLATFORM}"
fi

echo "=== Building Optio Agent Images ==="
echo "Tag: ${TAG}"

# Base image (all others depend on this)
echo "[1/10] Building optio-base..."
docker build -t "optio-base:${TAG}" -f "${SCRIPT_DIR}/base.Dockerfile" "${ROOT_DIR}"
docker build ${PLATFORM_FLAG} -t "optio-base:${TAG}" -f "${SCRIPT_DIR}/base.Dockerfile" "${ROOT_DIR}"

echo "Using base internal image: optio-base:${TAG}"
BASE_IMAGE="optio-base:${TAG}"

# Language-specific images (can be built in parallel)
echo "[2/10] Building optio-node..."
docker build -t "optio-node:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/node.Dockerfile" "${ROOT_DIR}" &
docker build ${PLATFORM_FLAG} -t "optio-node:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/node.Dockerfile" "${ROOT_DIR}" &

echo "[3/10] Building optio-python..."
docker build -t "optio-python:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/python.Dockerfile" "${ROOT_DIR}" &
docker build ${PLATFORM_FLAG} -t "optio-python:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/python.Dockerfile" "${ROOT_DIR}" &

echo "[4/10] Building optio-go..."
docker build -t "optio-go:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/go.Dockerfile" "${ROOT_DIR}" &
docker build ${PLATFORM_FLAG} -t "optio-go:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/go.Dockerfile" "${ROOT_DIR}" &

echo "[5/10] Building optio-rust..."
docker build -t "optio-rust:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/rust.Dockerfile" "${ROOT_DIR}" &
docker build ${PLATFORM_FLAG} -t "optio-rust:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/rust.Dockerfile" "${ROOT_DIR}" &

echo "[6/10] Building optio-ruby..."
docker build -t "optio-ruby:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/ruby.Dockerfile" "${ROOT_DIR}" &
Expand All @@ -34,15 +60,15 @@ echo "[7/10] Building optio-dart..."
docker build -t "optio-dart:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/dart.Dockerfile" "${ROOT_DIR}" &

echo "[8/10] Building optio-dind..."
docker build -t "optio-dind:${TAG}" -f "${SCRIPT_DIR}/dind.Dockerfile" "${ROOT_DIR}" &
docker build ${PLATFORM_FLAG} -t "optio-dind:${TAG}" -f "${SCRIPT_DIR}/dind.Dockerfile" "${ROOT_DIR}" &

echo "[9/10] Building optio-optio (operations assistant)..."
docker build -t "optio-optio:${TAG}" -f "${ROOT_DIR}/Dockerfile.optio" "${ROOT_DIR}" &
docker build ${PLATFORM_FLAG} -t "optio-optio:${TAG}" -f "${ROOT_DIR}/Dockerfile.optio" "${ROOT_DIR}" &

wait

echo "[10/10] Building optio-full..."
docker build -t "optio-full:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/full.Dockerfile" "${ROOT_DIR}"
docker build ${PLATFORM_FLAG} -t "optio-full:${TAG}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -f "${SCRIPT_DIR}/full.Dockerfile" "${ROOT_DIR}"

# Tag optio-base as the default
docker tag "optio-base:${TAG}" "optio-agent:${TAG}"
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-adapters/src/gemini.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ describe("GeminiAdapter", () => {
expect(settingsFile).toBeDefined();
const settings = JSON.parse(settingsFile!.content);
expect(settings.security.auth.selectedType).toBe("gemini-api-key");
expect(settings.general.defaultApprovalMode).toBe("yolo");
// "yolo" is not a valid settings.json enum; it's applied via the CLI --approval-mode flag.
// Settings file uses "auto_edit" as the nearest valid equivalent.
expect(settings.general.defaultApprovalMode).toBe("auto_edit");
expect(settings.telemetry.enabled).toBe(false);
});

Expand Down
5 changes: 4 additions & 1 deletion packages/agent-adapters/src/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,13 @@ export class GeminiAdapter implements AgentAdapter {
const approvalMode = input.geminiApprovalMode ?? "yolo";
const authType = input.geminiAuthMode === "vertex-ai" ? "vertex-ai" : "gemini-api-key";
const maxSessionTurns = input.maxTurnsCoding ?? 250;
// "yolo" is not a valid settings.json enum (valid: 'default' | 'auto_edit' | 'plan').
// Yolo mode is applied via the --approval-mode CLI flag; settings.json uses "auto_edit".
const settingsApprovalMode = approvalMode === "yolo" ? "auto_edit" : approvalMode;
const geminiSettings = {
security: { auth: { selectedType: authType } },
model: { maxSessionTurns },
general: { defaultApprovalMode: approvalMode },
general: { defaultApprovalMode: settingsApprovalMode },
telemetry: { enabled: false },
};
setupFiles.push({
Expand Down
9 changes: 5 additions & 4 deletions packages/shared/src/utils/off-peak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ function getETDate(ref: Date, targetHourET: number): Date {
`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${String(targetHourET).padStart(2, "0")}:00:00`,
);

// Get what hour ET thinks this is
const { hour: guessHour } = getETComponents(guess);
const drift = guessHour - targetHourET;
guess.setHours(guess.getHours() - drift);
// Get what ET thinks this is, including minutes (sub-hour timezone offsets like IST = UTC+5:30
// would otherwise introduce a 30-minute drift when using setHours in local time).
const { hour: guessHour, minute: guessMinute } = getETComponents(guess);
const driftMs = (guessHour - targetHourET) * 60 * 60 * 1000 + guessMinute * 60 * 1000;
guess.setTime(guess.getTime() - driftMs);

return guess;
}
Expand Down
Loading